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 546fd64e perf(parquet/pqarrow): write FixedSizeBinary values directly 
(#1263)
546fd64e is described below

commit 546fd64ede25a89fc84b682ec87ab985a369d238
Author: Minh Vu <[email protected]>
AuthorDate: Mon Aug 31 23:24:43 2026 +0200

    perf(parquet/pqarrow): write FixedSizeBinary values directly (#1263)
    
    ## Summary
    
    - **Writes Arrow FixedSizeBinary values directly** from the fixed-width
    value buffer.
    - Skips null slots with validity bit runs.
    - Keeps stats, bloom filters, byte-stream-split, slices, nested lists,
    and dictionary paths covered.
    
    ## Benchmark
    
    Apple M1 Pro. 64K rows. 16-byte values. Median of 3 runs.
    
    | case | upstream main | this PR | change |
    | --- | ---: | ---: | ---: |
    | required, stats off | 1.77 ms, 5.84 MB | 0.50 ms, 4.26 MB | -72% time,
    -27% bytes |
    | required, stats on | 2.28 ms, 5.85 MB | 1.14 ms, 4.28 MB | -50% time,
    -27% bytes |
    | nullable, stats off | 1.92 ms, 5.95 MB | 1.13 ms, 4.38 MB | -41% time,
    -26% bytes |
    | nullable, stats on | 2.42 ms, 5.95 MB | 1.78 ms, 4.39 MB | -27% time,
    -26% bytes |
    
    Command:
    
    ```text
    go test ./parquet/pqarrow -run "^$" -bench 
"^BenchmarkWriteArrowFixedSizeBinary$" -benchmem -benchtime=2s -count=3
    ```
    
    ## Tests
    
    - `PARQUET_TEST_DATA=/path/to/parquet-testing/data go test
    ./parquet/...`
    - `PARQUET_TEST_DATA=/path/to/parquet-testing/data go test -race
    ./parquet/internal/encoding ./parquet/file ./parquet/metadata
    ./parquet/pqarrow`
    - `go vet ./parquet/internal/encoding ./parquet/file ./parquet/metadata
    ./parquet/pqarrow`
---
 .../fixed_len_byte_array_column_writer_arrow.go    | 161 +++++++++++++
 ...ixed_len_byte_array_column_writer_arrow_test.go |  62 +++++
 .../encoding/fixed_len_byte_array_encoder.go       |  43 ++++
 .../encoding/fixed_len_byte_array_encoder_test.go  |  26 +++
 parquet/metadata/fixed_len_byte_array_arrow.go     | 144 ++++++++++++
 .../metadata/fixed_len_byte_array_arrow_test.go    |  69 ++++++
 parquet/pqarrow/encode_arrow.go                    |  15 ++
 parquet/pqarrow/fixed_size_binary_bench_test.go    |  90 ++++++++
 parquet/pqarrow/fixed_size_binary_test.go          | 257 +++++++++++++++++++++
 9 files changed, 867 insertions(+)

diff --git a/parquet/file/fixed_len_byte_array_column_writer_arrow.go 
b/parquet/file/fixed_len_byte_array_column_writer_arrow.go
new file mode 100644
index 00000000..8acb31b1
--- /dev/null
+++ b/parquet/file/fixed_len_byte_array_column_writer_arrow.go
@@ -0,0 +1,161 @@
+// 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 fixedLenByteArrayArrowEncoder interface {
+       PutArrow([]byte)
+       PutArrowSpaced([]byte, []byte, int64)
+}
+
+// SupportsArrowValues reports whether the active fixed-length byte-array
+// encoder and configured batch size support writing an Arrow value buffer 
directly.
+func (w *FixedLenByteArrayColumnChunkWriter) SupportsArrowValues() bool {
+       if _, ok := w.currentEncoder.(fixedLenByteArrayArrowEncoder); !ok {
+               return false
+       }
+       typeLen := int64(w.descr.TypeLength())
+       batchSize := w.props.WriteBatchSize()
+       const maxSafeBatchDataSize int64 = 1 << 30
+       if typeLen <= 0 || batchSize <= 0 || batchSize > max(1, 
maxSafeBatchDataSize/(typeLen+4)) {
+               return false
+       }
+       if w.pageStatistics == nil {
+               return true
+       }
+       _, ok := w.pageStatistics.(*metadata.FixedLenByteArrayStatistics)
+       return ok
+}
+
+func (w *FixedLenByteArrayColumnChunkWriter) writeArrowValues(values []byte, 
byteWidth int, numNulls int64) {
+       w.currentEncoder.(fixedLenByteArrayArrowEncoder).PutArrow(values)
+       if stats, ok := 
w.pageStatistics.(*metadata.FixedLenByteArrayStatistics); ok {
+               stats.UpdateFromArrowFixedWidth(values, byteWidth, numNulls)
+       }
+       if w.bloomFilter != nil && w.currentEncoder.Encoding() != 
parquet.Encodings.PlainDict {
+               metadata.InsertArrowFixedLenHashes(w.bloomFilter, values, 
byteWidth)
+       }
+}
+
+func (w *FixedLenByteArrayColumnChunkWriter) writeArrowValuesSpaced(values 
[]byte, byteWidth int, numRead, numValues int64, validBits []byte, 
validBitsOffset int64) {
+       enc := w.currentEncoder.(fixedLenByteArrayArrowEncoder)
+       numSpaced := int64(len(values) / byteWidth)
+       if numSpaced == numRead {
+               enc.PutArrow(values)
+       } else {
+               enc.PutArrowSpaced(values, validBits, validBitsOffset)
+       }
+
+       if stats, ok := 
w.pageStatistics.(*metadata.FixedLenByteArrayStatistics); ok {
+               stats.UpdateFromArrowFixedWidthSpaced(values, byteWidth, 
validBits, validBitsOffset, numSpaced-numRead)
+               stats.IncNulls(numValues - numSpaced)
+       }
+       if w.bloomFilter != nil && w.currentEncoder.Encoding() != 
parquet.Encodings.PlainDict {
+               metadata.InsertSpacedArrowFixedLenHashes(w.bloomFilter, 
numRead, values, byteWidth, validBits, validBitsOffset)
+       }
+}
+
+// WriteBatchArrow writes fixed-length byte-array values directly from an Arrow
+// value buffer. The buffer contains typeLength bytes per value.
+func (w *FixedLenByteArrayColumnChunkWriter) WriteBatchArrow(values []byte, 
defLevels, repLevels []int16) (valueOffset int64, err error) {
+       defer func() {
+               if r := recover(); r != nil {
+                       err = utils.FormatRecoveredError("unknown error type", 
r)
+               }
+       }()
+       if !w.SupportsArrowValues() {
+               return 0, fmt.Errorf("parquet: current fixed-length byte-array 
encoder does not support Arrow values")
+       }
+
+       typeLen := int(w.descr.TypeLength())
+       if typeLen <= 0 || len(values)%typeLen != 0 {
+               return 0, fmt.Errorf("parquet: Arrow fixed-length values are 
not aligned to the type length")
+       }
+       length := len(values) / typeLen
+       if defLevels != nil {
+               length = len(defLevels)
+       }
+       if length == 0 {
+               return 0, nil
+       }
+
+       w.doBatches(int64(length), repLevels, func(offset, batch int64) {
+               toWrite := w.writeLevels(batch, levelSliceOrNil(defLevels, 
offset, batch), levelSliceOrNil(repLevels, offset, batch))
+               start := int(valueOffset) * typeLen
+               end := int(valueOffset+toWrite) * typeLen
+               w.writeArrowValues(values[start:end], typeLen, batch-toWrite)
+               if err := w.commitWriteAndCheckPageLimit(batch, toWrite); err 
!= nil {
+                       panic(err)
+               }
+               valueOffset += toWrite
+               w.checkDictionarySizeLimit()
+       })
+       return valueOffset, nil
+}
+
+// WriteBatchSpacedArrow writes fixed-length byte-array values directly from an
+// Arrow value buffer while using validBits to skip null values.
+func (w *FixedLenByteArrayColumnChunkWriter) WriteBatchSpacedArrow(values 
[]byte, 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 !w.SupportsArrowValues() {
+               return 0, fmt.Errorf("parquet: current fixed-length byte-array 
encoder does not support Arrow values")
+       }
+
+       typeLen := int(w.descr.TypeLength())
+       if typeLen <= 0 || len(values)%typeLen != 0 {
+               return 0, fmt.Errorf("parquet: Arrow fixed-length values are 
not aligned to the type length")
+       }
+       length := len(defLevels)
+       if defLevels == nil {
+               length = len(values) / typeLen
+       }
+       if length == 0 {
+               return 0, nil
+       }
+
+       w.doBatches(int64(length), repLevels, func(offset, batch int64) {
+               info := w.maybeCalculateValidityBits(levelSliceOrNil(defLevels, 
offset, batch), batch)
+               w.writeLevelsSpaced(batch, levelSliceOrNil(defLevels, offset, 
batch), levelSliceOrNil(repLevels, offset, batch))
+
+               start := int(valueOffset) * typeLen
+               end := int(valueOffset+info.numSpaced()) * typeLen
+               writeBits := validBits
+               writeBitsOffset := validBitsOffset + valueOffset
+               if w.bitsBuffer != nil {
+                       writeBits = w.bitsBuffer.Bytes()
+                       writeBitsOffset = 0
+               }
+               w.writeArrowValuesSpaced(values[start:end], typeLen, 
info.batchNum, batch, writeBits, writeBitsOffset)
+               if err := w.commitWriteAndCheckPageLimit(batch, 
info.numSpaced()); err != nil {
+                       panic(err)
+               }
+               valueOffset += info.numSpaced()
+               w.checkDictionarySizeLimit()
+       })
+       return valueOffset, nil
+}
diff --git a/parquet/file/fixed_len_byte_array_column_writer_arrow_test.go 
b/parquet/file/fixed_len_byte_array_column_writer_arrow_test.go
new file mode 100644
index 00000000..54eca951
--- /dev/null
+++ b/parquet/file/fixed_len_byte_array_column_writer_arrow_test.go
@@ -0,0 +1,62 @@
+// 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"
+       "testing"
+
+       "github.com/apache/arrow-go/v18/arrow/memory"
+       "github.com/apache/arrow-go/v18/parquet"
+       "github.com/apache/arrow-go/v18/parquet/internal/encoding"
+       "github.com/apache/arrow-go/v18/parquet/schema"
+       "github.com/stretchr/testify/require"
+)
+
+func TestFixedLenByteArraySupportsArrowValuesBatchLimit(t *testing.T) {
+       for _, tc := range []struct {
+               byteWidth int32
+               limit     int64
+       }{
+               {byteWidth: 3, limit: 153391689},
+               {byteWidth: 1 << 20, limit: 1023},
+               {byteWidth: 1 << 30, limit: 1},
+       } {
+               byteWidth, limit := tc.byteWidth, tc.limit
+               for _, batchSize := range []int64{-1, 0, limit, limit + 1} {
+                       t.Run(fmt.Sprintf("width-%d/batch-%d", byteWidth, 
batchSize), func(t *testing.T) {
+                               node, err := schema.NewPrimitiveNode("value", 
parquet.Repetitions.Required, parquet.Types.FixedLenByteArray, -1, byteWidth)
+                               require.NoError(t, err)
+                               descr := schema.NewColumn(node, 0, 0)
+                               enc := 
encoding.NewEncoder(parquet.Types.FixedLenByteArray, parquet.Encodings.Plain, 
false, descr, memory.DefaultAllocator)
+                               defer enc.Release()
+                               writer := 
&FixedLenByteArrayColumnChunkWriter{columnWriter: columnWriter{
+                                       descr:          descr,
+                                       props:          
parquet.NewWriterProperties(parquet.WithBatchSize(batchSize)),
+                                       currentEncoder: enc,
+                               }}
+                               require.Equal(t, batchSize == limit, 
writer.SupportsArrowValues())
+                               if batchSize != limit {
+                                       _, err := writer.WriteBatchArrow(nil, 
nil, nil)
+                                       require.ErrorContains(t, err, "does not 
support Arrow values")
+                                       _, err = 
writer.WriteBatchSpacedArrow(nil, nil, nil, nil, 0)
+                                       require.ErrorContains(t, err, "does not 
support Arrow values")
+                               }
+                       })
+               }
+       }
+}
diff --git a/parquet/internal/encoding/fixed_len_byte_array_encoder.go 
b/parquet/internal/encoding/fixed_len_byte_array_encoder.go
index e4c3c650..cd3e0a84 100644
--- a/parquet/internal/encoding/fixed_len_byte_array_encoder.go
+++ b/parquet/internal/encoding/fixed_len_byte_array_encoder.go
@@ -56,6 +56,20 @@ func (enc *PlainFixedLenByteArrayEncoder) Put(in 
[]parquet.FixedLenByteArray) {
        }
 }
 
+// PutArrow writes fixed-width values already laid out in an Arrow value 
buffer.
+// The buffer contains typeLen bytes for each value.
+func (enc *PlainFixedLenByteArrayEncoder) PutArrow(values []byte) {
+       if len(values) == 0 {
+               return
+       }
+       if enc.typeLen <= 0 || len(values)%enc.typeLen != 0 {
+               panic("parquet: Arrow fixed-length values are not aligned to 
the type length")
+       }
+
+       enc.sink.Reserve(len(values))
+       enc.sink.UnsafeWrite(values)
+}
+
 func (enc *PlainFixedLenByteArrayEncoder) Release() {
        enc.encoder.Release()
        enc.zeroValue = nil
@@ -82,6 +96,35 @@ func (enc *PlainFixedLenByteArrayEncoder) PutSpaced(in 
[]parquet.FixedLenByteArr
        }
 }
 
+// PutArrowSpaced writes fixed-width values from an Arrow value buffer while
+// skipping values whose corresponding validity bits are unset.
+func (enc *PlainFixedLenByteArrayEncoder) PutArrowSpaced(values []byte, 
validBits []byte, validBitsOffset int64) {
+       if validBits == nil {
+               enc.PutArrow(values)
+               return
+       }
+       if enc.typeLen <= 0 || len(values)%enc.typeLen != 0 {
+               panic("parquet: Arrow fixed-length values are not aligned to 
the type length")
+       }
+
+       nvalues := int64(len(values) / enc.typeLen)
+       if enc.bitSetReader == nil {
+               enc.bitSetReader = bitutils.NewSetBitRunReader(validBits, 
validBitsOffset, nvalues)
+       } else {
+               enc.bitSetReader.Reset(validBits, validBitsOffset, nvalues)
+       }
+
+       for {
+               run := enc.bitSetReader.NextRun()
+               if run.Length == 0 {
+                       break
+               }
+               start := int(run.Pos) * enc.typeLen
+               end := int(run.Pos+run.Length) * enc.typeLen
+               enc.PutArrow(values[start:end])
+       }
+}
+
 // Type returns the underlying physical type this encoder works with, Fixed 
Length byte arrays.
 func (PlainFixedLenByteArrayEncoder) Type() parquet.Type {
        return parquet.Types.FixedLenByteArray
diff --git a/parquet/internal/encoding/fixed_len_byte_array_encoder_test.go 
b/parquet/internal/encoding/fixed_len_byte_array_encoder_test.go
index 1edee315..9c6240d5 100644
--- a/parquet/internal/encoding/fixed_len_byte_array_encoder_test.go
+++ b/parquet/internal/encoding/fixed_len_byte_array_encoder_test.go
@@ -102,3 +102,29 @@ func TestPlainFixedLenByteArrayEncoder_ReusesZeroValue(t 
*testing.T) {
        encoder.Put([]parquet.FixedLenByteArray{nil})
        require.Same(t, &zeroValue[0], &encoder.zeroValue[0])
 }
+
+func TestPlainFixedLenByteArrayEncoder_PutArrow(t *testing.T) {
+       sink := NewPooledBufferWriter(0)
+       elem := schema.NewFixedLenByteArrayNode("test", 
parquet.Repetitions.Required, 4, 0)
+       descr := schema.NewColumn(elem, 0, 0)
+       encoder := &PlainFixedLenByteArrayEncoder{
+               encoder: encoder{
+                       descr:   descr,
+                       typeLen: 4,
+                       sink:    sink,
+               },
+       }
+       defer encoder.Release()
+
+       values := []byte("abcdefghijklmnop")
+       encoder.PutArrow(values)
+       require.Equal(t, values, sink.Bytes())
+
+       sink.Reset(0)
+       encoder.PutArrowSpaced(values, []byte{0b0101}, 0)
+       require.Equal(t, []byte("abcdijkl"), sink.Bytes())
+
+       sink.Reset(0)
+       encoder.PutArrowSpaced(values, []byte{0b1010}, 1)
+       require.Equal(t, []byte("abcdijkl"), sink.Bytes())
+}
diff --git a/parquet/metadata/fixed_len_byte_array_arrow.go 
b/parquet/metadata/fixed_len_byte_array_arrow.go
new file mode 100644
index 00000000..88bae9b3
--- /dev/null
+++ b/parquet/metadata/fixed_len_byte_array_arrow.go
@@ -0,0 +1,144 @@
+// 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"
+)
+
+func arrowFixedLenValueCount(values []byte, byteWidth int) int64 {
+       if byteWidth <= 0 || len(values)%byteWidth != 0 {
+               panic("parquet: Arrow fixed-length values are not aligned to 
the type length")
+       }
+       return int64(len(values) / byteWidth)
+}
+
+// UpdateFromArrowFixedWidth updates fixed-length byte-array statistics from
+// values laid out in an Arrow value buffer.
+func (s *FixedLenByteArrayStatistics) UpdateFromArrowFixedWidth(values []byte, 
byteWidth int, numNull int64) {
+       nvalues := arrowFixedLenValueCount(values, byteWidth)
+       s.IncNulls(numNull)
+       s.nvalues += nvalues
+       if nvalues == 0 {
+               return
+       }
+
+       min, max := s.defaultMin(), s.defaultMax()
+       for offset := 0; offset < len(values); offset += byteWidth {
+               value := parquet.FixedLenByteArray(values[offset : 
offset+byteWidth])
+               min = s.minval(min, value)
+               max = s.maxval(max, value)
+       }
+       s.SetMinMax(min, max)
+}
+
+// UpdateFromArrowFixedWidthSpaced updates fixed-length byte-array statistics
+// from an Arrow value buffer whose null positions are described by validBits.
+func (s *FixedLenByteArrayStatistics) UpdateFromArrowFixedWidthSpaced(values 
[]byte, byteWidth int, validBits []byte, validBitsOffset, numNull int64) {
+       nvalues := arrowFixedLenValueCount(values, byteWidth)
+       if validBits == nil {
+               s.UpdateFromArrowFixedWidth(values, byteWidth, numNull)
+               return
+       }
+
+       s.IncNulls(numNull)
+       s.nvalues += nvalues - numNull
+       if nvalues == 0 || nvalues == numNull {
+               return
+       }
+
+       if s.bitSetReader == nil {
+               s.bitSetReader = bitutils.NewSetBitRunReader(validBits, 
validBitsOffset, nvalues)
+       } else {
+               s.bitSetReader.Reset(validBits, validBitsOffset, nvalues)
+       }
+
+       min, max := s.defaultMin(), s.defaultMax()
+       for {
+               run := s.bitSetReader.NextRun()
+               if run.Length == 0 {
+                       break
+               }
+               for pos := run.Pos; pos < run.Pos+run.Length; pos++ {
+                       start := int(pos) * byteWidth
+                       value := parquet.FixedLenByteArray(values[start : 
start+byteWidth])
+                       min = s.minval(min, value)
+                       max = s.maxval(max, value)
+               }
+       }
+       s.SetMinMax(min, max)
+}
+
+// InsertArrowFixedLenHashes inserts hashes for fixed-length values laid out in
+// an Arrow value buffer.
+func InsertArrowFixedLenHashes(b BloomFilterBuilder, values []byte, byteWidth 
int) {
+       if len(values) == 0 {
+               return
+       }
+       arrowFixedLenValueCount(values, byteWidth)
+
+       h := b.Hasher()
+       var (
+               byteBatch [bloomFilterHashBatchSize][]byte
+               hashBatch [bloomFilterHashBatchSize]uint64
+       )
+       for offset := 0; offset < len(values); offset += 
bloomFilterHashBatchSize * byteWidth {
+               end := min(offset+bloomFilterHashBatchSize*byteWidth, 
len(values))
+               n := (end - offset) / byteWidth
+               for i := 0; i < n; i++ {
+                       start := offset + i*byteWidth
+                       byteBatch[i] = values[start : start+byteWidth]
+               }
+               b.InsertBulk(sum64s(h, byteBatch[:n], hashBatch[:n]))
+       }
+}
+
+// InsertSpacedArrowFixedLenHashes inserts hashes for valid fixed-length values
+// from an Arrow value buffer.
+func InsertSpacedArrowFixedLenHashes(b BloomFilterBuilder, numValid int64, 
values []byte, byteWidth int, validBits []byte, validBitsOffset int64) {
+       if numValid == 0 {
+               return
+       }
+       if validBits == nil {
+               InsertArrowFixedLenHashes(b, values, byteWidth)
+               return
+       }
+
+       nvalues := arrowFixedLenValueCount(values, byteWidth)
+       h := b.Hasher()
+       var (
+               byteBatch [bloomFilterHashBatchSize][]byte
+               hashBatch [bloomFilterHashBatchSize]uint64
+       )
+       setReader := bitutils.NewSetBitRunReader(validBits, validBitsOffset, 
nvalues)
+       for {
+               run := setReader.NextRun()
+               if run.Length == 0 {
+                       break
+               }
+               for pos := run.Pos; pos < run.Pos+run.Length; pos += 
bloomFilterHashBatchSize {
+                       end := min(pos+int64(bloomFilterHashBatchSize), 
run.Pos+run.Length)
+                       n := int(end - pos)
+                       for i := 0; i < n; i++ {
+                               start := int(pos+int64(i)) * byteWidth
+                               byteBatch[i] = values[start : start+byteWidth]
+                       }
+                       b.InsertBulk(sum64s(h, byteBatch[:n], hashBatch[:n]))
+               }
+       }
+}
diff --git a/parquet/metadata/fixed_len_byte_array_arrow_test.go 
b/parquet/metadata/fixed_len_byte_array_arrow_test.go
new file mode 100644
index 00000000..6ad5779d
--- /dev/null
+++ b/parquet/metadata/fixed_len_byte_array_arrow_test.go
@@ -0,0 +1,69 @@
+// 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 TestFixedLenByteArrayStatisticsUpdateFromArrowFixedWidth(t *testing.T) {
+       node, err := schema.NewPrimitiveNode("value", 
parquet.Repetitions.Optional, parquet.Types.FixedLenByteArray, -1, 3)
+       require.NoError(t, err)
+       descr := schema.NewColumn(node, 0, 0)
+       stats := NewStatistics(descr, 
memory.DefaultAllocator).(*FixedLenByteArrayStatistics)
+
+       values := []byte("bbb" + "aaa" + "ccc" + "abc")
+       stats.UpdateFromArrowFixedWidth(values, 3, 0)
+
+       require.Equal(t, int64(4), stats.NumValues())
+       require.Equal(t, int64(0), stats.NullCount())
+       require.Equal(t, []byte("aaa"), []byte(stats.Min()))
+       require.Equal(t, []byte("ccc"), []byte(stats.Max()))
+
+       validBits := []byte{0b1101}
+       stats.Reset()
+       stats.UpdateFromArrowFixedWidthSpaced(values, 3, validBits, 0, 1)
+       require.Equal(t, int64(3), stats.NumValues())
+       require.Equal(t, int64(1), stats.NullCount())
+       require.Equal(t, []byte("abc"), []byte(stats.Min()))
+       require.Equal(t, []byte("ccc"), []byte(stats.Max()))
+}
+
+func TestInsertArrowFixedLenHashes(t *testing.T) {
+       values := []byte("aaaa" + "bbbb" + "cccc" + "dddd")
+       parquetValues := []parquet.FixedLenByteArray{
+               values[0:4], values[4:8], values[8:12], values[12:16],
+       }
+
+       bloom := newBatchRecordingBloomFilter(xxhasher{})
+       InsertArrowFixedLenHashes(bloom, values, 4)
+       require.Equal(t, GetHashes(xxhasher{}, parquetValues), 
flattenHashBatches(bloom.batches))
+
+       validBits := make([]byte, bitutil.BytesForBits(6))
+       bitutil.SetBit(validBits, 2)
+       bitutil.SetBit(validBits, 4)
+       valid := []parquet.FixedLenByteArray{parquetValues[1], parquetValues[3]}
+       bloom = newBatchRecordingBloomFilter(xxhasher{})
+       InsertSpacedArrowFixedLenHashes(bloom, 2, values, 4, validBits, 1)
+       require.Equal(t, GetHashes(xxhasher{}, valid), 
flattenHashBatches(bloom.batches))
+}
diff --git a/parquet/pqarrow/encode_arrow.go b/parquet/pqarrow/encode_arrow.go
index 8b49fb95..3824aa98 100644
--- a/parquet/pqarrow/encode_arrow.go
+++ b/parquet/pqarrow/encode_arrow.go
@@ -686,6 +686,21 @@ func writeDenseArrow(ctx *arrowWriteContext, cw 
file.ColumnChunkWriter, leafArr
        case *file.FixedLenByteArrayColumnChunkWriter:
                switch dt := leafArr.DataType().(type) {
                case *arrow.FixedSizeBinaryType:
+                       if wr.SupportsArrowValues() {
+                               buffer := leafArr.Data().Buffers()[1]
+                               var valueBuf []byte
+                               if buffer != nil {
+                                       start := leafArr.Data().Offset() * 
dt.ByteWidth
+                                       end := start + 
leafArr.Len()*dt.ByteWidth
+                                       valueBuf = buffer.Bytes()[start:end]
+                               }
+                               if !maybeParentNulls && noNulls {
+                                       _, err = wr.WriteBatchArrow(valueBuf, 
defLevels, repLevels)
+                               } else {
+                                       _, err = 
wr.WriteBatchSpacedArrow(valueBuf, defLevels, repLevels, 
leafArr.NullBitmapBytes(), int64(leafArr.Data().Offset()))
+                               }
+                               return err
+                       }
                        data := make([]parquet.FixedLenByteArray, leafArr.Len())
                        for idx := range data {
                                data[idx] = 
leafArr.(*array.FixedSizeBinary).Value(idx)
diff --git a/parquet/pqarrow/fixed_size_binary_bench_test.go 
b/parquet/pqarrow/fixed_size_binary_bench_test.go
new file mode 100644
index 00000000..035438e1
--- /dev/null
+++ b/parquet/pqarrow/fixed_size_binary_bench_test.go
@@ -0,0 +1,90 @@
+// 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 benchmarkFixedSizeBinaryTable(mem memory.Allocator, n, byteWidth int, 
nullable bool) (arrow.Table, int64) {
+       builder := array.NewFixedSizeBinaryBuilder(mem, 
&arrow.FixedSizeBinaryType{ByteWidth: byteWidth})
+       builder.Reserve(n)
+       for i := 0; i < n; i++ {
+               if nullable && i%10 == 0 {
+                       builder.AppendNull()
+                       continue
+               }
+
+               value := make([]byte, byteWidth)
+               copy(value, strconv.AppendInt(nil, int64(i), 10))
+               builder.Append(value)
+       }
+       arr := builder.NewArray()
+       builder.Release()
+
+       sch := arrow.NewSchema([]arrow.Field{{
+               Name:     "value",
+               Type:     &arrow.FixedSizeBinaryType{ByteWidth: byteWidth},
+               Nullable: nullable,
+       }}, nil)
+       col := arrow.NewColumnFromArr(sch.Field(0), arr)
+       arr.Release()
+       tbl := array.NewTable(sch, []arrow.Column{col}, int64(n))
+       col.Release()
+       return tbl, int64(n * byteWidth)
+}
+
+func BenchmarkWriteArrowFixedSizeBinary(b *testing.B) {
+       const (
+               n         = 64 * 1024
+               byteWidth = 16
+       )
+       mem := memory.DefaultAllocator
+
+       for _, nullable := range []bool{false, true} {
+               tbl, inputBytes := benchmarkFixedSizeBinaryTable(mem, n, 
byteWidth, nullable)
+               b.Run("nullable="+strconv.FormatBool(nullable), func(b 
*testing.B) {
+                       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/fixed_size_binary_test.go 
b/parquet/pqarrow/fixed_size_binary_test.go
new file mode 100644
index 00000000..58c0c36e
--- /dev/null
+++ b/parquet/pqarrow/fixed_size_binary_test.go
@@ -0,0 +1,257 @@
+// 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 fixedSizeBinaryTable(t *testing.T, values [][]byte, byteWidth int) 
arrow.Table {
+       t.Helper()
+       mem := memory.DefaultAllocator
+       builder := array.NewFixedSizeBinaryBuilder(mem, 
&arrow.FixedSizeBinaryType{ByteWidth: byteWidth})
+       for _, value := range values {
+               if value == nil {
+                       builder.AppendNull()
+               } else {
+                       builder.Append(value)
+               }
+       }
+       arr := builder.NewArray()
+       builder.Release()
+
+       field := arrow.Field{
+               Name:     "value",
+               Type:     &arrow.FixedSizeBinaryType{ByteWidth: byteWidth},
+               Nullable: true,
+       }
+       sch := arrow.NewSchema([]arrow.Field{field}, nil)
+       col := arrow.NewColumnFromArr(field, arr)
+       arr.Release()
+       tbl := array.NewTable(sch, []arrow.Column{col}, int64(len(values)))
+       col.Release()
+       return tbl
+}
+
+func TestWriteArrowFixedSizeBinaryDirect(t *testing.T) {
+       tbl := fixedSizeBinaryTable(t, [][]byte{
+               {0x03, 0x02, 0x01},
+               nil,
+               {0x09, 0x08, 0x07},
+               {0x06, 0x05, 0x04},
+               {0x0c, 0x0b, 0x0a},
+               {0x0f, 0x0e, 0x0d},
+       }, 3)
+       defer tbl.Release()
+
+       for _, encoding := range []parquet.Encoding{parquet.Encodings.Plain, 
parquet.Encodings.ByteStreamSplit} {
+               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(16),
+                               parquet.WithPageIndexEnabled(true),
+                               parquet.WithBloomFilterEnabledFor("value", 
true),
+                               parquet.WithBloomFilterNDVFor("value", 
tbl.NumRows()),
+                       )
+                       data := writeParquetTable(t, tbl, tbl.NumRows(), 
writerProps)
+                       got := readParquetTable(t, data, 
pqarrow.ArrowReadProperties{})
+                       defer got.Release()
+                       assertTableColumnsEqual(t, tbl, got)
+               })
+       }
+}
+
+func TestWriteArrowFixedSizeBinaryDirectWithSlice(t *testing.T) {
+       full := fixedSizeBinaryTable(t, [][]byte{
+               {0x00, 0x01, 0x02, 0x03},
+               {0x04, 0x05, 0x06, 0x07},
+               {0x08, 0x09, 0x0a, 0x0b},
+               nil,
+               {0x10, 0x11, 0x12, 0x13},
+               {0x14, 0x15, 0x16, 0x17},
+       }, 4)
+       defer full.Release()
+
+       sliced := array.NewSlice(full.Column(0).Data().Chunk(0), 1, 5)
+       defer sliced.Release()
+       field := arrow.Field{Name: "value", Type: 
&arrow.FixedSizeBinaryType{ByteWidth: 4}, Nullable: true}
+       sch := arrow.NewSchema([]arrow.Field{field}, nil)
+       col := arrow.NewColumnFromArr(field, sliced)
+       tbl := array.NewTable(sch, []arrow.Column{col}, int64(sliced.Len()))
+       col.Release()
+       defer tbl.Release()
+
+       writerProps := parquet.NewWriterProperties(
+               parquet.WithDictionaryDefault(false),
+               parquet.WithBatchSize(2),
+               parquet.WithStats(true),
+       )
+       data := writeParquetTable(t, tbl, tbl.NumRows(), writerProps)
+       got := readParquetTable(t, data, pqarrow.ArrowReadProperties{})
+       defer got.Release()
+       assertTableColumnsEqual(t, tbl, got)
+}
+
+func TestWriteArrowFixedSizeBinaryDirectNestedList(t *testing.T) {
+       mem := memory.DefaultAllocator
+       dtype := &arrow.FixedSizeBinaryType{ByteWidth: 3}
+       builder := array.NewListBuilder(mem, dtype)
+       values := builder.ValueBuilder().(*array.FixedSizeBinaryBuilder)
+
+       builder.Append(true)
+       values.Append([]byte("aaa"))
+       values.Append([]byte("bbb"))
+       builder.AppendNull()
+       builder.Append(true)
+       builder.Append(true)
+       values.Append([]byte("ccc"))
+       values.AppendNull()
+       arr := builder.NewListArray()
+       builder.Release()
+
+       field := arrow.Field{Name: "value", Type: arr.DataType(), Nullable: 
true}
+       sch := arrow.NewSchema([]arrow.Field{field}, nil)
+       col := arrow.NewColumnFromArr(field, arr)
+       arr.Release()
+       tbl := array.NewTable(sch, []arrow.Column{col}, 4)
+       col.Release()
+       defer tbl.Release()
+
+       writerProps := parquet.NewWriterProperties(
+               parquet.WithDictionaryDefault(false),
+               parquet.WithBatchSize(2),
+               parquet.WithStats(true),
+               parquet.WithPageIndexEnabled(true),
+       )
+       data := writeParquetTable(t, tbl, tbl.NumRows(), writerProps)
+       got := readParquetTable(t, data, pqarrow.ArrowReadProperties{})
+       defer got.Release()
+       assertTableColumnsEqual(t, tbl, got)
+}
+
+func TestWriteArrowFixedSizeBinaryDictionaryFallbackPath(t *testing.T) {
+       tbl := fixedSizeBinaryTable(t, [][]byte{
+               []byte("foo!"),
+               []byte("bar!"),
+               nil,
+               []byte("foo!"),
+               []byte("baz!"),
+       }, 4)
+       defer tbl.Release()
+
+       writerProps := parquet.NewWriterProperties(
+               parquet.WithDictionaryDefault(true),
+               parquet.WithStats(true),
+       )
+       data := writeParquetTable(t, tbl, tbl.NumRows(), writerProps)
+       got := readParquetTable(t, data, pqarrow.ArrowReadProperties{})
+       defer got.Release()
+       assertTableColumnsEqual(t, tbl, got)
+}
+
+func TestWriteArrowFixedSizeBinaryAllNull(t *testing.T) {
+       tbl := fixedSizeBinaryTable(t, [][]byte{nil, nil, nil, nil}, 8)
+       defer tbl.Release()
+
+       writerProps := parquet.NewWriterProperties(
+               parquet.WithDictionaryDefault(false),
+               parquet.WithStats(true),
+       )
+       data := writeParquetTable(t, tbl, tbl.NumRows(), writerProps)
+       got := readParquetTable(t, data, pqarrow.ArrowReadProperties{})
+       defer got.Release()
+       assertTableColumnsEqual(t, tbl, got)
+       require.Equal(t, tbl.NumRows(), got.NumRows())
+}
+
+func TestWriteArrowFixedSizeBinaryNestedNullStatistics(t *testing.T) {
+       for _, pageVersion := range 
[]parquet.DataPageVersion{parquet.DataPageV1, parquet.DataPageV2} {
+               for _, nullableValues := range []bool{false, true} {
+                       t.Run(fmt.Sprintf("page-%d/nullable-values-%t", 
pageVersion, nullableValues), func(t *testing.T) {
+                               builder := 
array.NewListBuilder(memory.DefaultAllocator, 
&arrow.FixedSizeBinaryType{ByteWidth: 3})
+                               defer builder.Release()
+                               values := 
builder.ValueBuilder().(*array.FixedSizeBinaryBuilder)
+                               builder.Append(true)
+                               values.Append([]byte("aaa"))
+                               builder.AppendNull()
+                               builder.Append(true)
+                               builder.Append(true)
+                               values.Append([]byte("zzz"))
+                               nullCount := int64(2)
+                               if nullableValues {
+                                       values.AppendNull()
+                                       nullCount++
+                               }
+                               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.WithBatchSize(2))
+                               data := writeParquetTable(t, tbl, 
tbl.NumRows(), props)
+                               reader, err := 
file.NewParquetReader(bytes.NewReader(data))
+                               require.NoError(t, err)
+                               defer reader.Close()
+                               chunk, err := 
reader.MetaData().RowGroup(0).ColumnChunk(0)
+                               require.NoError(t, err)
+                               stats, err := chunk.Statistics()
+                               require.NoError(t, err)
+                               require.Equal(t, nullCount, stats.NullCount())
+                               require.Equal(t, int64(2), stats.NumValues())
+                               require.Equal(t, []byte("aaa"), 
stats.EncodeMin())
+                               require.Equal(t, []byte("zzz"), 
stats.EncodeMax())
+                       })
+               }
+       }
+}
+
+func TestWriteArrowFixedSizeBinaryBatchSizeFallback(t *testing.T) {
+       for _, batchSize := range []int64{-1, 0, (1<<30)/7 + 1} {
+               for _, nullable := range []bool{false, true} {
+                       t.Run(fmt.Sprintf("batch-%d/nullable-%t", batchSize, 
nullable), func(t *testing.T) {
+                               values := [][]byte{[]byte("aaa"), []byte("zzz")}
+                               if nullable {
+                                       values = append(values, nil)
+                               }
+                               tbl := fixedSizeBinaryTable(t, values, 3)
+                               defer tbl.Release()
+                               props := 
parquet.NewWriterProperties(parquet.WithDictionaryDefault(false),
+                                       parquet.WithBatchSize(batchSize))
+                               data := writeParquetTable(t, tbl, 
tbl.NumRows(), props)
+                               got := readParquetTable(t, data, 
pqarrow.ArrowReadProperties{})
+                               defer got.Release()
+                               assertTableColumnsEqual(t, tbl, got)
+                       })
+               }
+       }
+}

Reply via email to