Repository: segmentio/parquet-go (go)

Issue:
Add a way to select `bytestreamsplit` in parquet struct tag
As of yet I don’t see any way to select this encoding in the struct tag.

Pull request description:
add the "split" struct field tag to configure the BYTE_STREAM_SPLIT encoding
Fixes #193 

Patch:
diff --git a/column_buffer.go b/column_buffer.go
index a5a3542..a4ff06c 100644
--- a/column_buffer.go
+++ b/column_buffer.go
@@ -792,16 +792,18 @@ func (col *booleanColumnBuffer) writeValues(rows array, size, offset uintptr) {
 		// First we attempt to write enough bits to align the number of values
 		// in the column buffer on 8 bytes. After this step the next bit should
 		// be written at the zero'th index of a byte of the buffer.
-		var b byte
-		for i < r {
-			v := *(*byte)(rows.index(i, size, offset))
-			b |= (v & 1) << uint(i)
-			i++
+		if r < 8 {
+			var b byte
+			for i < r {
+				v := *(*byte)(rows.index(i, size, offset))
+				b |= (v & 1) << uint(i)
+				i++
+			}
+			x := uint(col.numValues) / 8
+			y := uint(col.numValues) % 8
+			col.bits[x] |= (b << y) | (col.bits[x] & ^(0xFF << y))
+			col.numValues += int32(i)
 		}
-		x := uint(col.numValues) / 8
-		y := uint(col.numValues) % 8
-		col.bits[x] |= (b << y) | (col.bits[x] & ^(0xFF << y))
-		col.numValues += int32(i)
 
 		if n := rows.len - i; n >= 8 {
 			// At this stage, we know that that we have at least 8 bits to write
@@ -810,26 +812,13 @@ func (col *booleanColumnBuffer) writeValues(rows array, size, offset uintptr) {
 			// packing them into a single byte and writing it to the output
 			// buffer. This effectively reduces by 87.5% the number of memory
 			// stores that the program needs to perform to generate the values.
-			for j := i + (n/8)*8; i < j; i += 8 {
-				b0 := *(*byte)(rows.index(i+0, size, offset))
-				b1 := *(*byte)(rows.index(i+1, size, offset))
-				b2 := *(*byte)(rows.index(i+2, size, offset))
-				b3 := *(*byte)(rows.index(i+3, size, offset))
-				b4 := *(*byte)(rows.index(i+4, size, offset))
-				b5 := *(*byte)(rows.index(i+5, size, offset))
-				b6 := *(*byte)(rows.index(i+6, size, offset))
-				b7 := *(*byte)(rows.index(i+7, size, offset))
-
-				col.bits[col.numValues/8] = (b0 & 1) |
-					((b1 & 1) << 1) |
-					((b2 & 1) << 2) |
-					((b3 & 1) << 3) |
-					((b4 & 1) << 4) |
-					((b5 & 1) << 5) |
-					((b6 & 1) << 6) |
-					((b7 & 1) << 7)
-				col.numValues += 8
+			section := array{
+				ptr: rows.index(i, size, 0),
+				len: ((rows.len - i) / 8) * 8,
 			}
+			writeValuesBool(col.bits[col.numValues/8:], section, size, offset)
+			col.numValues += int32(section.len)
+			i += section.len
 		}
 	}
 
@@ -939,14 +928,9 @@ func (col *int32ColumnBuffer) writeValues(rows array, size, offset uintptr) {
 	if n := len(col.values) + rows.len; n > cap(col.values) {
 		col.values = append(make([]int32, 0, max(n, 2*cap(col.values))), col.values...)
 	}
-
 	n := len(col.values)
 	col.values = col.values[:n+rows.len]
-
-	values := col.values[n:]
-	for i := range values {
-		values[i] = *(*int32)(rows.index(i, size, offset))
-	}
+	writeValuesInt32(col.values[n:], rows, size, offset)
 }
 
 func (col *int32ColumnBuffer) ReadValuesAt(values []Value, offset int64) (n int, err error) {
@@ -1043,14 +1027,9 @@ func (col *int64ColumnBuffer) writeValues(rows array, size, offset uintptr) {
 	if n := len(col.values) + rows.len; n > cap(col.values) {
 		col.values = append(make([]int64, 0, max(n, 2*cap(col.values))), col.values...)
 	}
-
 	n := len(col.values)
 	col.values = col.values[:n+rows.len]
-
-	values := col.values[n:]
-	for i := range values {
-		values[i] = *(*int64)(rows.index(i, size, offset))
-	}
+	writeValuesInt64(col.values[n:], rows, size, offset)
 }
 
 func (col *int64ColumnBuffer) ReadValuesAt(values []Value, offset int64) (n int, err error) {
@@ -1241,14 +1220,9 @@ func (col *floatColumnBuffer) writeValues(rows array, size, offset uintptr) {
 	if n := len(col.values) + rows.len; n > cap(col.values) {
 		col.values = append(make([]float32, 0, max(n, 2*cap(col.values))), col.values...)
 	}
-
 	n := len(col.values)
 	col.values = col.values[:n+rows.len]
-
-	values := col.values[n:]
-	for i := range values {
-		values[i] = *(*float32)(rows.index(i, size, offset))
-	}
+	writeValuesFloat32(col.values[n:], rows, size, offset)
 }
 
 func (col *floatColumnBuffer) ReadValuesAt(values []Value, offset int64) (n int, err error) {
@@ -1345,14 +1319,9 @@ func (col *doubleColumnBuffer) writeValues(rows array, size, offset uintptr) {
 	if n := len(col.values) + rows.len; n > cap(col.values) {
 		col.values = append(make([]float64, 0, max(n, 2*cap(col.values))), col.values...)
 	}
-
 	n := len(col.values)
 	col.values = col.values[:n+rows.len]
-
-	values := col.values[n:]
-	for i := range values {
-		values[i] = *(*float64)(rows.index(i, size, offset))
-	}
+	writeValuesFloat64(col.values[n:], rows, size, offset)
 }
 
 func (col *doubleColumnBuffer) ReadValuesAt(values []Value, offset int64) (n int, err error) {
@@ -1627,21 +1596,15 @@ func (col *fixedLenByteArrayColumnBuffer) writeValues(rows array, size, offset u
 	}
 }
 
-func (col *fixedLenByteArrayColumnBuffer) writeValues128(rows array, size, offset uintptr) {
+func (col *fixedLenByteArrayColumnBuffer) writeValuesUint128(rows array, size, offset uintptr) {
 	c := cap(col.data)
 	n := len(col.data) + (16 * rows.len)
 	if c < n {
 		col.data = append(make([]byte, 0, max(n, 2*c)), col.data...)
 	}
-
 	firstIndex := len(col.data)
 	col.data = col.data[:len(col.data)+(16*rows.len)]
-
-	data := unsafe.Pointer(&col.data[firstIndex])
-	for i := 0; i < rows.len; i++ {
-		p := rows.index(i, size, offset)
-		*(*[16]byte)(unsafe.Add(data, i*16)) = *(*[16]byte)(p)
-	}
+	writeValuesUint128(col.data[firstIndex:], rows, size, offset)
 }
 
 func (col *fixedLenByteArrayColumnBuffer) ReadValuesAt(values []Value, offset int64) (n int, err error) {
@@ -1738,14 +1701,9 @@ func (col *uint32ColumnBuffer) writeValues(rows array, size, offset uintptr) {
 	if n := len(col.values) + rows.len; n > cap(col.values) {
 		col.values = append(make([]uint32, 0, max(n, 2*cap(col.values))), col.values...)
 	}
-
 	n := len(col.values)
 	col.values = col.values[:n+rows.len]
-
-	values := col.values[n:]
-	for i := range values {
-		values[i] = *(*uint32)(rows.index(i, size, offset))
-	}
+	writeValuesUint32(col.values[n:], rows, size, offset)
 }
 
 func (col *uint32ColumnBuffer) ReadValuesAt(values []Value, offset int64) (n int, err error) {
@@ -1842,14 +1800,9 @@ func (col *uint64ColumnBuffer) writeValues(rows array, size, offset uintptr) {
 	if n := len(col.values) + rows.len; n > cap(col.values) {
 		col.values = append(make([]uint64, 0, max(n, 2*cap(col.values))), col.values...)
 	}
-
 	n := len(col.values)
 	col.values = col.values[:n+rows.len]
-
-	values := col.values[n:]
-	for i := range values {
-		values[i] = *(*uint64)(rows.index(i, size, offset))
-	}
+	writeValuesUint64(col.values[n:], rows, size, offset)
 }
 
 func (col *uint64ColumnBuffer) ReadValuesAt(values []Value, offset int64) (n int, err error) {
diff --git a/column_buffer_amd64.go b/column_buffer_amd64.go
new file mode 100644
index 0000000..976791d
--- /dev/null
+++ b/column_buffer_amd64.go
@@ -0,0 +1,55 @@
+//go:build !purego
+
+package parquet
+
+import (
+	"unsafe"
+
+	"golang.org/x/sys/cpu"
+)
+
+var hasAVX2 = cpu.X86.HasAVX2
+
+//go:noescape
+func writeValuesBitpack(values unsafe.Pointer, rows array, size, offset uintptr)
+
+//go:noescape
+func writeValues32bits(values unsafe.Pointer, rows array, size, offset uintptr)
+
+//go:noescpae
+func writeValues64bits(values unsafe.Pointer, rows array, size, offset uintptr)
+
+//go:noescape
+func writeValues128bits(values unsafe.Pointer, rows array, size, offset uintptr)
+
+func writeValuesBool(values []byte, rows array, size, offset uintptr) {
+	writeValuesBitpack(*(*unsafe.Pointer)(unsafe.Pointer(&values)), rows, size, offset)
+}
+
+func writeValuesInt32(values []int32, rows array, size, offset uintptr) {
+	writeValues32bits(*(*unsafe.Pointer)(unsafe.Pointer(&values)), rows, size, offset)
+}
+
+func writeValuesInt64(values []int64, rows array, size, offset uintptr) {
+	writeValues64bits(*(*unsafe.Pointer)(unsafe.Pointer(&values)), rows, size, offset)
+}
+
+func writeValuesUint32(values []uint32, rows array, size, offset uintptr) {
+	writeValues32bits(*(*unsafe.Pointer)(unsafe.Pointer(&values)), rows, size, offset)
+}
+
+func writeValuesUint64(values []uint64, rows array, size, offset uintptr) {
+	writeValues64bits(*(*unsafe.Pointer)(unsafe.Pointer(&values)), rows, size, offset)
+}
+
+func writeValuesUint128(values []byte, rows array, size, offset uintptr) {
+	writeValues128bits(*(*unsafe.Pointer)(unsafe.Pointer(&values)), rows, size, offset)
+}
+
+func writeValuesFloat32(values []float32, rows array, size, offset uintptr) {
+	writeValues32bits(*(*unsafe.Pointer)(unsafe.Pointer(&values)), rows, size, offset)
+}
+
+func writeValuesFloat64(values []float64, rows array, size, offset uintptr) {
+	writeValues64bits(*(*unsafe.Pointer)(unsafe.Pointer(&values)), rows, size, offset)
+}
diff --git a/column_buffer_amd64.s b/column_buffer_amd64.s
new file mode 100644
index 0000000..dbcdd98
--- /dev/null
+++ b/column_buffer_amd64.s
@@ -0,0 +1,268 @@
+//go:build !purego
+
+#include "textflag.h"
+
+// func writeValuesBitpack(values unsafe.Pointer, rows array, size, offset uintptr)
+TEXT ·writeValuesBitpack(SB), NOSPLIT, $0-40
+    MOVQ values_base+0(FP), AX
+    MOVQ rows_base+8(FP), BX
+    MOVQ rows_len+16(FP), CX
+    MOVQ size+24(FP), DX
+    MOVQ offset+32(FP), DI
+
+    CMPQ CX, $0
+    JNE init
+    RET
+init:
+    ADDQ DI, BX
+    SHRQ $3, CX
+    XORQ SI, SI
+
+    CMPB ·hasAVX2(SB), $0
+    JE loop
+
+    // Make sure `size - offset` is at least 4 bytes, otherwise VPGATHERDD
+    // may read data beyond the end of the program memory and trigger a fault.
+    //
+    // If the boolean values do not have enough padding we must fallback to the
+    // scalar algorithm to be able to load single bytes from memory.
+    MOVQ DX, R8
+    SUBQ DI, R8
+    CMPQ R8, $4
+    JB loop
+
+    VPBROADCASTD size+24(FP), Y0
+    VPMULLD scale8x4<>(SB), Y0, Y0
+    VPCMPEQD Y1, Y1, Y1
+    VPCMPEQD Y2, Y2, Y2
+    VPCMPEQD Y3, Y3, Y3
+    VPSRLD $31, Y3, Y3
+avx2loop:
+    VPGATHERDD Y1, (BX)(Y0*1), Y4
+    VMOVDQU Y2, Y1
+    VPAND Y3, Y4, Y4
+    VPSLLD $31, Y4, Y4
+    VMOVMSKPS Y4, DI
+
+    MOVB DI, (AX)(SI*1)
+
+    LEAQ (BX)(DX*8), BX
+    INCQ SI
+    CMPQ SI, CX
+    JNE avx2loop
+    VZEROUPPER
+    RET
+loop:
+    LEAQ (BX)(DX*2), DI
+    MOVBQZX (BX), R8
+    MOVBQZX (BX)(DX*1), R9
+    MOVBQZX (DI), R10
+    MOVBQZX (DI)(DX*1), R11
+    LEAQ (BX)(DX*4), BX
+    LEAQ (DI)(DX*4), DI
+    MOVBQZX (BX), R12
+    MOVBQZX (BX)(DX*1), R13
+    MOVBQZX (DI), R14
+    MOVBQZX (DI)(DX*1), R15
+    LEAQ (BX)(DX*4), BX
+
+    ANDQ $1, R8
+    ANDQ $1, R9
+    ANDQ $1, R10
+    ANDQ $1, R11
+    ANDQ $1, R12
+    ANDQ $1, R13
+    ANDQ $1, R14
+    ANDQ $1, R15
+
+    SHLQ $1, R9
+    SHLQ $2, R10
+    SHLQ $3, R11
+    SHLQ $4, R12
+    SHLQ $5, R13
+    SHLQ $6, R14
+    SHLQ $7, R15
+
+    ORQ R9, R8
+    ORQ R11, R10
+    ORQ R13, R12
+    ORQ R15, R14
+    ORQ R10, R8
+    ORQ R12, R8
+    ORQ R14, R8
+
+    MOVB R8, (AX)(SI*1)
+
+    INCQ SI
+    CMPQ SI, CX
+    JNE loop
+    RET
+
+// func writeValues32bits(values unsafe.Pointer, rows array, size, offset uintptr)
+TEXT ·writeValues32bits(SB), NOSPLIT, $0-40
+    MOVQ values_base+0(FP), AX
+    MOVQ rows_base+8(FP), BX
+    MOVQ rows_len+16(FP), CX
+    MOVQ size+24(FP), DX
+
+    XORQ SI, SI
+    ADDQ offset+32(FP), BX
+
+    CMPQ CX, $0
+    JE done
+
+    CMPQ CX, $8
+    JB loop1x4
+
+    CMPB ·hasAVX2(SB), $0
+    JE loop1x4
+
+    MOVQ CX, DI
+    SHRQ $3, DI
+    SHLQ $3, DI
+
+    VPBROADCASTD size+24(FP), Y0
+    VPMULLD scale8x4<>(SB), Y0, Y0
+    VPCMPEQD Y1, Y1, Y1
+    VPCMPEQD Y2, Y2, Y2
+loop8x4:
+    VPGATHERDD Y1, (BX)(Y0*1), Y3
+    VMOVDQU Y3, (AX)(SI*4)
+    VMOVDQU Y2, Y1
+
+    LEAQ (BX)(DX*8), BX
+    ADDQ $8, SI
+    CMPQ SI, DI
+    JNE loop8x4
+    VZEROUPPER
+
+    CMPQ SI, CX
+    JE done
+
+loop1x4:
+    MOVL (BX), R8
+    MOVL R8, (AX)(SI*4)
+
+    ADDQ DX, BX
+    INCQ SI
+    CMPQ SI, CX
+    JNE loop1x4
+done:
+    RET
+
+// func writeValues64bits(values unsafe.Pointer, rows array, size, offset uintptr)
+TEXT ·writeValues64bits(SB), NOSPLIT, $0-40
+    MOVQ values_base+0(FP), AX
+    MOVQ rows_base+8(FP), BX
+    MOVQ rows_len+16(FP), CX
+    MOVQ size+24(FP), DX
+
+    XORQ SI, SI
+    ADDQ offset+32(FP), BX
+
+    CMPQ CX, $0
+    JE done
+
+    CMPQ CX, $4
+    JB loop1x8
+
+    CMPB ·hasAVX2(SB), $0
+    JE loop1x8
+
+    MOVQ CX, DI
+    SHRQ $2, DI
+    SHLQ $2, DI
+
+    VPBROADCASTQ size+24(FP), Y0
+    VPMULLD scale4x8<>(SB), Y0, Y0
+    VPCMPEQD Y1, Y1, Y1
+    VPCMPEQD Y2, Y2, Y2
+loop4x8:
+    VPGATHERQQ Y1, (BX)(Y0*1), Y3
+    VMOVDQU Y3, (AX)(SI*8)
+    VMOVDQU Y2, Y1
+
+    LEAQ (BX)(DX*4), BX
+    ADDQ $4, SI
+    CMPQ SI, DI
+    JNE loop4x8
+    VZEROUPPER
+
+    CMPQ SI, CX
+    JE done
+loop1x8:
+    MOVQ (BX), R8
+    MOVQ R8, (AX)(SI*8)
+
+    ADDQ DX, BX
+    INCQ SI
+    CMPQ SI, CX
+    JNE loop1x8
+done:
+    RET
+
+// func writeValues128bits(values unsafe.Pointer, rows array, size, offset uintptr)
+TEXT ·writeValues128bits(SB), NOSPLIT, $0-40
+    MOVQ values_base+0(FP), AX
+    MOVQ rows_base+8(FP), BX
+    MOVQ rows_len+16(FP), CX
+    MOVQ size+24(FP), DX
+
+    XORQ SI, SI
+    ADDQ offset+32(FP), BX
+
+    CMPQ CX, $0
+    JE done
+
+    CMPQ CX, $2
+    JB loop1x16
+
+    CMPB ·hasAVX2(SB), $0
+    JE loop1x16
+
+    MOVQ CX, DI
+    SHRQ $1, DI
+    SHLQ $1, DI
+loop2x16:
+    VMOVDQU (BX), X0
+    VMOVDQU (BX)(DX*1), X1
+
+    VMOVDQU X0, (AX)
+    VMOVDQU X1, 16(AX)
+
+    ADDQ $32, AX
+    LEAQ (BX)(DX*2), BX
+    ADDQ $2, SI
+    CMPQ SI, DI
+    JNE loop2x16
+    VZEROUPPER
+
+    CMPQ SI, CX
+    JE done
+loop1x16:
+    MOVOU (BX), X0
+    MOVOU X0, (AX)
+
+    ADDQ $16, AX
+    ADDQ DX, BX
+    INCQ SI
+    CMPQ SI, CX
+    JNE loop1x16
+done:
+    RET
+
+GLOBL scale8x4<>(SB), RODATA|NOPTR, $32
+DATA scale8x4<>+0(SB)/4,  $0
+DATA scale8x4<>+4(SB)/4,  $1
+DATA scale8x4<>+8(SB)/4,  $2
+DATA scale8x4<>+12(SB)/4, $3
+DATA scale8x4<>+16(SB)/4, $4
+DATA scale8x4<>+20(SB)/4, $5
+DATA scale8x4<>+24(SB)/4, $6
+DATA scale8x4<>+28(SB)/4, $7
+
+GLOBL scale4x8<>(SB), RODATA|NOPTR, $32
+DATA scale4x8<>+0(SB)/8,  $0
+DATA scale4x8<>+8(SB)/8,  $1
+DATA scale4x8<>+16(SB)/8, $2
+DATA scale4x8<>+24(SB)/8, $3
diff --git a/column_buffer_go18.go b/column_buffer_go18.go
index 6cbe9f8..10523a0 100644
--- a/column_buffer_go18.go
+++ b/column_buffer_go18.go
@@ -850,7 +850,7 @@ func (w *columnBufferWriter) writeRowsUUID(rows array, size, offset uintptr, lev
 
 	switch c := w.columns[levels.columnIndex].(type) {
 	case *fixedLenByteArrayColumnBuffer:
-		c.writeValues128(rows, size, offset)
+		c.writeValuesUint128(rows, size, offset)
 	default:
 		w.reset()
 		for i := 0; i < rows.len; i++ {
diff --git a/column_buffer_purego.go b/column_buffer_purego.go
new file mode 100644
index 0000000..dd4303c
--- /dev/null
+++ b/column_buffer_purego.go
@@ -0,0 +1,72 @@
+//go:build !amd64 || purego
+
+package parquet
+
+import "unsafe"
+
+func writeValuesBool(values []byte, rows array, size, offset uintptr) {
+	for i, j := 0, 0; i < rows.len; i += 8 {
+		b0 := *(*byte)(rows.index(i+0, size, offset))
+		b1 := *(*byte)(rows.index(i+1, size, offset))
+		b2 := *(*byte)(rows.index(i+2, size, offset))
+		b3 := *(*byte)(rows.index(i+3, size, offset))
+		b4 := *(*byte)(rows.index(i+4, size, offset))
+		b5 := *(*byte)(rows.index(i+5, size, offset))
+		b6 := *(*byte)(rows.index(i+6, size, offset))
+		b7 := *(*byte)(rows.index(i+7, size, offset))
+
+		values[j] = (b0 & 1) |
+			((b1 & 1) << 1) |
+			((b2 & 1) << 2) |
+			((b3 & 1) << 3) |
+			((b4 & 1) << 4) |
+			((b5 & 1) << 5) |
+			((b6 & 1) << 6) |
+			((b7 & 1) << 7)
+		j++
+	}
+}
+
+func writeValuesInt32(values []int32, rows array, size, offset uintptr) {
+	for i := range values {
+		values[i] = *(*int32)(rows.index(i, size, offset))
+	}
+}
+
+func writeValuesInt64(values []int64, rows array, size, offset uintptr) {
+	for i := range values {
+		values[i] = *(*int64)(rows.index(i, size, offset))
+	}
+}
+
+func writeValuesUint32(values []uint32, rows array, size, offset uintptr) {
+	for i := range values {
+		values[i] = *(*uint32)(rows.index(i, size, offset))
+	}
+}
+
+func writeValuesUint64(values []uint64, rows array, size, offset uintptr) {
+	for i := range values {
+		values[i] = *(*uint64)(rows.index(i, size, offset))
+	}
+}
+
+func writeValuesUint128(values []byte, rows array, size, offset uintptr) {
+	data := unsafe.Pointer(&values[0])
+	for i := 0; i < rows.len; i++ {
+		p := rows.index(i, size, offset)
+		*(*[16]byte)(unsafe.Add(data, i*16)) = *(*[16]byte)(p)
+	}
+}
+
+func writeValuesFloat32(values []float32, rows array, size, offset uintptr) {
+	for i := range values {
+		values[i] = *(*float32)(rows.index(i, size, offset))
+	}
+}
+
+func writeValuesFloat64(values []float64, rows array, size, offset uintptr) {
+	for i := range values {
+		values[i] = *(*float64)(rows.index(i, size, offset))
+	}
+}
diff --git a/internal/quick/quick.go b/internal/quick/quick.go
new file mode 100644
index 0000000..bce7ef0
--- /dev/null
+++ b/internal/quick/quick.go
@@ -0,0 +1,174 @@
+package quick
+
+import (
+	"fmt"
+	"math/rand"
+	"reflect"
+	"strings"
+)
+
+var DefaultConfig = Config{
+	Sizes: []int{
+		0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
+		10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
+		20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
+		30, 31, 32, 33, 34, 35, 36, 37, 38, 39,
+		99, 100, 101,
+		127, 128, 129,
+		255, 256, 257,
+		1000, 1023, 1024, 1025,
+		2000, 2095, 2048, 2049,
+		4000, 4095, 4096, 4097,
+	},
+	Seed: 0,
+}
+
+// Check is inspired by the standard quick.Check package, but enhances the
+// API and tests arrays of larger sizes than the maximum of 50 hardcoded in
+// testing/quick.
+func Check(f interface{}) error {
+	return DefaultConfig.Check(f)
+}
+
+type Config struct {
+	Sizes []int
+	Seed  int64
+}
+
+func (c *Config) Check(f interface{}) error {
+	v := reflect.ValueOf(f)
+	r := rand.New(rand.NewSource(c.Seed))
+	t := v.Type().In(0)
+
+	makeValue := MakeValueFuncOf(t.Elem())
+	makeArray := func(n int) reflect.Value {
+		array := reflect.MakeSlice(t, n, n)
+		for i := 0; i < n; i++ {
+			makeValue(array.Index(i), r)
+		}
+		return array
+	}
+
+	if makeArray == nil {
+		panic("cannot run quick check on function with input of type " + v.Type().In(0).String())
+	}
+
+	for _, n := range c.Sizes {
+		for i := 0; i < 3; i++ {
+			in := makeArray(n)
+			ok := v.Call([]reflect.Value{in})
+			if !ok[0].Bool() {
+				return fmt.Errorf("test #%d: failed on input of size %d: %#v\n", i+1, n, in.Interface())
+			}
+		}
+	}
+	return nil
+
+}
+
+type MakeValueFunc func(reflect.Value, *rand.Rand)
+
+func MakeValueFuncOf(t reflect.Type) MakeValueFunc {
+	switch t.Kind() {
+	case reflect.Bool:
+		return func(v reflect.Value, r *rand.Rand) {
+			v.SetBool((r.Int() % 2) != 0)
+		}
+
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		return func(v reflect.Value, r *rand.Rand) {
+			v.SetInt(r.Int63())
+		}
+
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+		return func(v reflect.Value, r *rand.Rand) {
+			v.SetUint(r.Uint64())
+		}
+
+	case reflect.Float32, reflect.Float64:
+		return func(v reflect.Value, r *rand.Rand) {
+			v.SetFloat(r.Float64())
+		}
+
+	case reflect.String:
+		return func(v reflect.Value, r *rand.Rand) {
+			const characters = "1234567890qwertyuiopasdfghjklzxcvbnm"
+			s := new(strings.Builder)
+			n := r.Intn(10)
+			for i := 0; i < n; i++ {
+				s.WriteByte(characters[i])
+			}
+			v.SetString(s.String())
+		}
+
+	case reflect.Array:
+		makeElem := MakeValueFuncOf(t.Elem())
+		return func(v reflect.Value, r *rand.Rand) {
+			for i, n := 0, v.Len(); i < n; i++ {
+				makeElem(v.Index(i), r)
+			}
+		}
+
+	case reflect.Slice:
+		switch e := t.Elem(); e.Kind() {
+		case reflect.Uint8:
+			return func(v reflect.Value, r *rand.Rand) {
+				b := make([]byte, r.Intn(50))
+				r.Read(b)
+				v.SetBytes(b)
+			}
+		default:
+			makeElem := MakeValueFuncOf(t.Elem())
+			return func(v reflect.Value, r *rand.Rand) {
+				n := r.Intn(10)
+				s := reflect.MakeSlice(t, n, n)
+				for i := 0; i < n; i++ {
+					makeElem(s.Index(i), r)
+				}
+				v.Set(s)
+			}
+		}
+
+	case reflect.Map:
+		makeKey := MakeValueFuncOf(t.Key())
+		makeElem := MakeValueFuncOf(t.Elem())
+		return func(v reflect.Value, r *rand.Rand) {
+			m := reflect.MakeMap(t)
+			n := r.Intn(10)
+			k := reflect.New(t.Key()).Elem()
+			e := reflect.New(t.Elem()).Elem()
+			for i := 0; i < n; i++ {
+				makeKey(k, r)
+				makeElem(e, r)
+				m.SetMapIndex(k, e)
+			}
+			v.Set(m)
+		}
+
+	case reflect.Struct:
+		fields := make([]reflect.StructField, 0, t.NumField())
+		makeValues := make([]MakeValueFunc, 0, cap(fields))
+		for i, n := 0, cap(fields); i < n; i++ {
+			if f := t.Field(i); f.PkgPath == "" { // skip unexported fields
+				fields = append(fields, f)
+				makeValues = append(makeValues, MakeValueFuncOf(f.Type))
+			}
+		}
+		return func(v reflect.Value, r *rand.Rand) {
+			for i := range fields {
+				makeValues[i](v.FieldByIndex(fields[i].Index), r)
+			}
+		}
+
+	case reflect.Ptr:
+		t = t.Elem()
+		makeValue := MakeValueFuncOf(t)
+		return func(v reflect.Value, r *rand.Rand) {
+			v.Set(reflect.New(t))
+			makeValue(v.Elem(), r)
+		}
+
+	default:
+		panic("quick.Check does not support test values of type " + t.String())
+	}
+}
diff --git a/schema.go b/schema.go
index d1b3b01..cd587fb 100644
--- a/schema.go
+++ b/schema.go
@@ -56,6 +56,7 @@ type Schema struct {
 //	decimal   | for int32, int64 and [n]byte types, use the parquet DECIMAL logical type
 //	date      | for int32 types use the DATE logical type
 //	timestamp | for int64 types use the TIMESTAMP logical type with millisecond precision
+//	split     | for float32/float64, use the BYTE_STREAM_SPLIT encoding
 //
 // The date logical type is an int32 value of the number of days since the unix epoch
 //
@@ -515,6 +516,14 @@ func makeStructField(f reflect.StructField) structField {
 				throwInvalidFieldTag(f, option)
 			}
 
+		case "split":
+			switch f.Type.Kind() {
+			case reflect.Float32, reflect.Float64:
+				setEncoding(&ByteStreamSplit)
+			default:
+				throwInvalidFieldTag(f, option)
+			}
+
 		case "list":
 			switch f.Type.Kind() {
 			case reflect.Slice:


Test patch:
diff --git a/bloom_test.go b/bloom_test.go
index 8ef0aa8..81f4c58 100644
--- a/bloom_test.go
+++ b/bloom_test.go
@@ -3,12 +3,12 @@ package parquet
 import (
 	"math/rand"
 	"testing"
-	"testing/quick"
 
 	"github.com/segmentio/parquet-go/bloom"
 	"github.com/segmentio/parquet-go/deprecated"
 	"github.com/segmentio/parquet-go/encoding/plain"
 	"github.com/segmentio/parquet-go/internal/bits"
+	"github.com/segmentio/parquet-go/internal/quick"
 )
 
 func TestSplitBlockFilter(t *testing.T) {
@@ -145,7 +145,7 @@ func TestSplitBlockFilter(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.scenario, func(t *testing.T) {
-			if err := quick.Check(test.function, nil); err != nil {
+			if err := quick.Check(test.function); err != nil {
 				t.Error(err)
 			}
 		})
diff --git a/buffer_go18_test.go b/buffer_go18_test.go
index 3d83e98..7848a52 100644
--- a/buffer_go18_test.go
+++ b/buffer_go18_test.go
@@ -9,7 +9,6 @@ import (
 	"math/rand"
 	"reflect"
 	"testing"
-	"testing/quick"
 
 	"github.com/segmentio/parquet-go"
 )
@@ -36,13 +35,13 @@ func TestGenericBuffer(t *testing.T) {
 	testGenericBuffer[nestedListColumn1](t)
 	testGenericBuffer[nestedListColumn](t)
 	testGenericBuffer[*contact](t)
+	testGenericBuffer[paddedBooleanColumn](t)
 }
 
 func testGenericBuffer[Row any](t *testing.T) {
-	var prng = rand.New(rand.NewSource(1))
 	var model Row
 	t.Run(reflect.TypeOf(model).Name(), func(t *testing.T) {
-		f := func(rows []Row) bool {
+		err := quickCheck(func(rows []Row) bool {
 			if len(rows) == 0 {
 				return true // TODO: fix support for parquet files with zero rows
 			}
@@ -51,8 +50,8 @@ func testGenericBuffer[Row any](t *testing.T) {
 				return false
 			}
 			return true
-		}
-		if err := quick.Check(f, &quick.Config{Rand: prng}); err != nil {
+		})
+		if err != nil {
 			t.Error(err)
 		}
 	})
@@ -110,6 +109,7 @@ func BenchmarkGenericBuffer(b *testing.B) {
 	benchmarkGenericBuffer[mapColumn](b)
 	benchmarkGenericBuffer[decimalColumn](b)
 	benchmarkGenericBuffer[contact](b)
+	benchmarkGenericBuffer[paddedBooleanColumn](b)
 }
 
 func benchmarkGenericBuffer[Row generator[Row]](b *testing.B) {
diff --git a/buffer_test.go b/buffer_test.go
index 77fa7e5..e1c4d62 100644
--- a/buffer_test.go
+++ b/buffer_test.go
@@ -10,7 +10,6 @@ import (
 	"sort"
 	"strconv"
 	"testing"
-	"testing/quick"
 
 	"github.com/segmentio/parquet-go"
 	"github.com/segmentio/parquet-go/encoding"
@@ -413,7 +412,7 @@ func TestBufferGenerateBloomFilters(t *testing.T) {
 		return true
 	}
 
-	if err := quick.Check(f, nil); err != nil {
+	if err := quickCheck(f); err != nil {
 		t.Error(err)
 	}
 }
diff --git a/column_test.go b/column_test.go
index b54db61..e2d7a7d 100644
--- a/column_test.go
+++ b/column_test.go
@@ -4,7 +4,6 @@ import (
 	"fmt"
 	"math/rand"
 	"testing"
-	"testing/quick"
 
 	"github.com/google/uuid"
 	"github.com/segmentio/parquet-go"
@@ -102,9 +101,7 @@ func TestColumnPageIndex(t *testing.T) {
 				},
 			} {
 				t.Run(test.scenario, func(t *testing.T) {
-					if err := quick.Check(test.function(t), &quick.Config{
-						Rand: rand.New(rand.NewSource(0)),
-					}); err != nil {
+					if err := quickCheck(test.function(t)); err != nil {
 						t.Error(err)
 					}
 				})
diff --git a/internal/bits/add_test.go b/internal/bits/add_test.go
index 04db284..d541732 100644
--- a/internal/bits/add_test.go
+++ b/internal/bits/add_test.go
@@ -5,10 +5,11 @@ import (
 	"testing"
 
 	"github.com/segmentio/parquet-go/internal/bits"
+	"github.com/segmentio/parquet-go/internal/quick"
 )
 
 func TestAddInt32(t *testing.T) {
-	err := quickCheck(func(values []int32) bool {
+	err := quick.Check(func(values []int32) bool {
 		const add = math.MaxInt32
 
 		valuesAndAdd := make([]int32, len(values))
@@ -32,7 +33,7 @@ func TestAddInt32(t *testing.T) {
 }
 
 func TestAddInt64(t *testing.T) {
-	err := quickCheck(func(values []int64) bool {
+	err := quick.Check(func(values []int64) bool {
 		const add = math.MaxInt64
 
 		valuesAndAdd := make([]int64, len(values))
diff --git a/internal/bits/bits_test.go b/internal/bits/bits_test.go
index 9612957..39f478e 100644
--- a/internal/bits/bits_test.go
+++ b/internal/bits/bits_test.go
@@ -2,8 +2,6 @@ package bits_test
 
 import (
 	"fmt"
-	"math/rand"
-	"reflect"
 	"testing"
 
 	"github.com/segmentio/parquet-go/internal/bits"
@@ -112,119 +110,3 @@ func forEachBenchmarkBufferSize(b *testing.B, f func(*testing.B, int)) {
 		})
 	}
 }
-
-// quickCheck is inspired by the standard quick.Check package, but enhances the
-// API and tests arrays of larger sizes than the maximum of 50 hardcoded in
-// testing/quick.
-func quickCheck(f interface{}) error {
-	v := reflect.ValueOf(f)
-	r := rand.New(rand.NewSource(0))
-
-	var makeArray func(int) interface{}
-	switch t := v.Type().In(0); t.Elem().Kind() {
-	case reflect.Bool:
-		makeArray = func(n int) interface{} {
-			v := make([]bool, n)
-			for i := range v {
-				v[i] = r.Int()%2 != 0
-			}
-			return v
-		}
-
-	case reflect.Int32:
-		makeArray = func(n int) interface{} {
-			v := make([]int32, n)
-			for i := range v {
-				v[i] = r.Int31()
-			}
-			return v
-		}
-
-	case reflect.Int64:
-		makeArray = func(n int) interface{} {
-			v := make([]int64, n)
-			for i := range v {
-				v[i] = r.Int63()
-			}
-			return v
-		}
-
-	case reflect.Uint32:
-		makeArray = func(n int) interface{} {
-			v := make([]uint32, n)
-			for i := range v {
-				v[i] = r.Uint32()
-			}
-			return v
-		}
-	case reflect.Uint64:
-		makeArray = func(n int) interface{} {
-			v := make([]uint64, n)
-			for i := range v {
-				v[i] = r.Uint64()
-			}
-			return v
-		}
-
-	case reflect.Float32:
-		makeArray = func(n int) interface{} {
-			v := make([]float32, n)
-			for i := range v {
-				v[i] = r.Float32()
-			}
-			return v
-		}
-
-	case reflect.Float64:
-		makeArray = func(n int) interface{} {
-			v := make([]float64, n)
-			for i := range v {
-				v[i] = r.Float64()
-			}
-			return v
-		}
-
-	case reflect.Uint8:
-		makeArray = func(n int) interface{} {
-			v := make([]byte, n)
-			r.Read(v)
-			return v
-		}
-
-	case reflect.Array:
-		e := t.Elem()
-		if e.Elem().Kind() == reflect.Uint8 && e.Len() == 16 {
-			makeArray = func(n int) interface{} {
-				v := make([]byte, n*16)
-				r.Read(v)
-				return bits.BytesToUint128(v)
-			}
-		}
-	}
-
-	if makeArray == nil {
-		panic("cannot run quick check on function with input of type " + v.Type().In(0).String())
-	}
-
-	for _, n := range [...]int{
-		0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
-		10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
-		20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
-		30, 31, 32, 33, 34, 35, 36, 37, 38, 39,
-		99, 100, 101,
-		127, 128, 129,
-		255, 256, 257,
-		1000, 1023, 1024, 1025,
-		2000, 2095, 2048, 2049,
-		4000, 4095, 4096, 4097,
-	} {
-		for i := 0; i < 3; i++ {
-			in := makeArray(n)
-			ok := v.Call([]reflect.Value{reflect.ValueOf(in)})
-			if !ok[0].Bool() {
-				return fmt.Errorf("test #%d: failed on input of size %d: %#v\n", i+1, n, in)
-			}
-		}
-	}
-	return nil
-}
diff --git a/internal/bits/count_test.go b/internal/bits/count_test.go
index 0e25338..1add19c 100644
--- a/internal/bits/count_test.go
+++ b/internal/bits/count_test.go
@@ -5,10 +5,11 @@ import (
 	"testing"
 
 	"github.com/segmentio/parquet-go/internal/bits"
+	"github.com/segmentio/parquet-go/internal/quick"
 )
 
 func TestCountByte(t *testing.T) {
-	err := quickCheck(func(data []byte) bool {
+	err := quick.Check(func(data []byte) bool {
 		data = bytes.Repeat(data, 8)
 		for _, c := range data {
 			n1 := bytes.Count(data, []byte{c})
diff --git a/internal/bits/max_test.go b/internal/bits/max_test.go
index c0b1668..f03c64a 100644
--- a/internal/bits/max_test.go
+++ b/internal/bits/max_test.go
@@ -6,10 +6,11 @@ import (
 	"testing"
 
 	"github.com/segmentio/parquet-go/internal/bits"
+	"github.com/segmentio/parquet-go/internal/quick"
 )
 
 func TestMaxBool(t *testing.T) {
-	err := quickCheck(func(values []bool) bool {
+	err := quick.Check(func(values []bool) bool {
 		max := false
 		for _, v := range values {
 			if v {
@@ -36,7 +37,7 @@ func TestMaxBool(t *testing.T) {
 }
 
 func TestMaxInt32(t *testing.T) {
-	err := quickCheck(func(values []int32) bool {
+	err := quick.Check(func(values []int32) bool {
 		max := int32(0)
 		if len(values) > 0 {
 			max = values[0]
@@ -54,7 +55,7 @@ func TestMaxInt32(t *testing.T) {
 }
 
 func TestMaxInt64(t *testing.T) {
-	err := quickCheck(func(values []int64) bool {
+	err := quick.Check(func(values []int64) bool {
 		max := int64(0)
 		if len(values) > 0 {
 			max = values[0]
@@ -72,7 +73,7 @@ func TestMaxInt64(t *testing.T) {
 }
 
 func TestMaxUint32(t *testing.T) {
-	err := quickCheck(func(values []uint32) bool {
+	err := quick.Check(func(values []uint32) bool {
 		max := uint32(0)
 		if len(values) > 0 {
 			max = values[0]
@@ -90,7 +91,7 @@ func TestMaxUint32(t *testing.T) {
 }
 
 func TestMaxUint64(t *testing.T) {
-	err := quickCheck(func(values []uint64) bool {
+	err := quick.Check(func(values []uint64) bool {
 		max := uint64(0)
 		if len(values) > 0 {
 			max = values[0]
@@ -108,7 +109,7 @@ func TestMaxUint64(t *testing.T) {
 }
 
 func TestMaxFloat32(t *testing.T) {
-	err := quickCheck(func(values []float32) bool {
+	err := quick.Check(func(values []float32) bool {
 		max := float32(0)
 		if len(values) > 0 {
 			max = values[0]
@@ -126,7 +127,7 @@ func TestMaxFloat32(t *testing.T) {
 }
 
 func TestMaxFloat64(t *testing.T) {
-	err := quickCheck(func(values []float64) bool {
+	err := quick.Check(func(values []float64) bool {
 		max := float64(0)
 		if len(values) > 0 {
 			max = values[0]
@@ -144,7 +145,7 @@ func TestMaxFloat64(t *testing.T) {
 }
 
 func TestMaxFixedLenByteArray1(t *testing.T) {
-	err := quickCheck(func(values []byte) bool {
+	err := quick.Check(func(values []byte) bool {
 		max := [1]byte{}
 		if len(values) > 0 {
 			max[0] = values[0]
@@ -163,7 +164,7 @@ func TestMaxFixedLenByteArray1(t *testing.T) {
 }
 
 func TestMaxFixedLenByteArray16(t *testing.T) {
-	err := quickCheck(func(values [][16]byte) bool {
+	err := quick.Check(func(values [][16]byte) bool {
 		max := [16]byte{}
 		if len(values) > 0 {
 			max = values[0]
diff --git a/internal/bits/min_test.go b/internal/bits/min_test.go
index 2f175cf..3daff50 100644
--- a/internal/bits/min_test.go
+++ b/internal/bits/min_test.go
@@ -6,10 +6,11 @@ import (
 	"testing"
 
 	"github.com/segmentio/parquet-go/internal/bits"
+	"github.com/segmentio/parquet-go/internal/quick"
 )
 
 func TestMinBool(t *testing.T) {
-	err := quickCheck(func(values []bool) bool {
+	err := quick.Check(func(values []bool) bool {
 		min := len(values) > 0
 		for _, v := range values {
 			if !v {
@@ -36,7 +37,7 @@ func TestMinBool(t *testing.T) {
 }
 
 func TestMinInt32(t *testing.T) {
-	err := quickCheck(func(values []int32) bool {
+	err := quick.Check(func(values []int32) bool {
 		min := int32(0)
 		if len(values) > 0 {
 			min = values[0]
@@ -54,7 +55,7 @@ func TestMinInt32(t *testing.T) {
 }
 
 func TestMinInt64(t *testing.T) {
-	err := quickCheck(func(values []int64) bool {
+	err := quick.Check(func(values []int64) bool {
 		min := int64(0)
 		if len(values) > 0 {
 			min = values[0]
@@ -72,7 +73,7 @@ func TestMinInt64(t *testing.T) {
 }
 
 func TestMinUint32(t *testing.T) {
-	err := quickCheck(func(values []uint32) bool {
+	err := quick.Check(func(values []uint32) bool {
 		min := uint32(0)
 		if len(values) > 0 {
 			min = values[0]
@@ -90,7 +91,7 @@ func TestMinUint32(t *testing.T) {
 }
 
 func TestMinUint64(t *testing.T) {
-	err := quickCheck(func(values []uint64) bool {
+	err := quick.Check(func(values []uint64) bool {
 		min := uint64(0)
 		if len(values) > 0 {
 			min = values[0]
@@ -108,7 +109,7 @@ func TestMinUint64(t *testing.T) {
 }
 
 func TestMinFloat32(t *testing.T) {
-	err := quickCheck(func(values []float32) bool {
+	err := quick.Check(func(values []float32) bool {
 		min := float32(0)
 		if len(values) > 0 {
 			min = values[0]
@@ -126,7 +127,7 @@ func TestMinFloat32(t *testing.T) {
 }
 
 func TestMinFloat64(t *testing.T) {
-	err := quickCheck(func(values []float64) bool {
+	err := quick.Check(func(values []float64) bool {
 		min := float64(0)
 		if len(values) > 0 {
 			min = values[0]
@@ -144,7 +145,7 @@ func TestMinFloat64(t *testing.T) {
 }
 
 func TestMinFixedLenByteArray1(t *testing.T) {
-	err := quickCheck(func(values []byte) bool {
+	err := quick.Check(func(values []byte) bool {
 		min := [1]byte{}
 		if len(values) > 0 {
 			min[0] = values[0]
@@ -163,7 +164,7 @@ func TestMinFixedLenByteArray1(t *testing.T) {
 }
 
 func TestMinFixedLenByteArray16(t *testing.T) {
-	err := quickCheck(func(values [][16]byte) bool {
+	err := quick.Check(func(values [][16]byte) bool {
 		min := [16]byte{}
 		if len(values) > 0 {
 			min = values[0]
diff --git a/internal/bits/minmax_test.go b/internal/bits/minmax_test.go
index 9d8c3b9..c006e52 100644
--- a/internal/bits/minmax_test.go
+++ b/internal/bits/minmax_test.go
@@ -6,12 +6,13 @@ import (
 	"testing"
 
 	"github.com/segmentio/parquet-go/internal/bits"
+	"github.com/segmentio/parquet-go/internal/quick"
 )
 
 const scaleFactor = 6000
 
 func TestMinMaxBool(t *testing.T) {
-	err := quickCheck(func(values []bool) bool {
+	err := quick.Check(func(values []bool) bool {
 		min := len(values) > 0
 		max := false
 		for _, v := range values {
@@ -41,7 +42,7 @@ func TestMinMaxBool(t *testing.T) {
 }
 
 func TestMinMaxInt32(t *testing.T) {
-	err := quickCheck(func(values []int32) bool {
+	err := quick.Check(func(values []int32) bool {
 		min := int32(0)
 		max := int32(0)
 		if len(values) > 0 {
@@ -65,7 +66,7 @@ func TestMinMaxInt32(t *testing.T) {
 }
 
 func TestMinMaxInt64(t *testing.T) {
-	err := quickCheck(func(values []int64) bool {
+	err := quick.Check(func(values []int64) bool {
 		min := int64(0)
 		max := int64(0)
 		if len(values) > 0 {
@@ -89,7 +90,7 @@ func TestMinMaxInt64(t *testing.T) {
 }
 
 func TestMinMaxUint32(t *testing.T) {
-	err := quickCheck(func(values []uint32) bool {
+	err := quick.Check(func(values []uint32) bool {
 		min := uint32(0)
 		max := uint32(0)
 		if len(values) > 0 {
@@ -113,7 +114,7 @@ func TestMinMaxUint32(t *testing.T) {
 }
 
 func TestMinMaxUint64(t *testing.T) {
-	err := quickCheck(func(values []uint64) bool {
+	err := quick.Check(func(values []uint64) bool {
 		min := uint64(0)
 		max := uint64(0)
 		if len(values) > 0 {
@@ -137,7 +138,7 @@ func TestMinMaxUint64(t *testing.T) {
 }
 
 func TestMinMaxFloat32(t *testing.T) {
-	err := quickCheck(func(values []float32) bool {
+	err := quick.Check(func(values []float32) bool {
 		min := float32(0)
 		max := float32(0)
 		if len(values) > 0 {
@@ -161,7 +162,7 @@ func TestMinMaxFloat32(t *testing.T) {
 }
 
 func TestMinMaxFloat64(t *testing.T) {
-	err := quickCheck(func(values []float64) bool {
+	err := quick.Check(func(values []float64) bool {
 		min := float64(0)
 		max := float64(0)
 		if len(values) > 0 {
@@ -185,7 +186,7 @@ func TestMinMaxFloat64(t *testing.T) {
 }
 
 func TestMinMaxFixedLenByteArray1(t *testing.T) {
-	err := quickCheck(func(values []byte) bool {
+	err := quick.Check(func(values []byte) bool {
 		min := [1]byte{}
 		max := [1]byte{}
 		if len(values) > 0 {
@@ -210,7 +211,7 @@ func TestMinMaxFixedLenByteArray1(t *testing.T) {
 }
 
 func TestMinMaxFixedLenByteArray16(t *testing.T) {
-	err := quickCheck(func(values [][16]byte) bool {
+	err := quick.Check(func(values [][16]byte) bool {
 		min := [16]byte{}
 		max := [16]byte{}
 		if len(values) > 0 {
diff --git a/internal/bits/order_test.go b/internal/bits/order_test.go
index 2533658..e3cff27 100644
--- a/internal/bits/order_test.go
+++ b/internal/bits/order_test.go
@@ -6,6 +6,7 @@ import (
 	"testing"
 
 	"github.com/segmentio/parquet-go/internal/bits"
+	"github.com/segmentio/parquet-go/internal/quick"
 )
 
 type boolOrder []bool
@@ -115,7 +116,7 @@ func TestOrderOfBool(t *testing.T) {
 	check := func(values []bool) bool {
 		return checkOrdering(t, boolOrder(values), bits.OrderOfBool(values))
 	}
-	err := quickCheck(func(values []bool) bool {
+	err := quick.Check(func(values []bool) bool {
 		if !check(values) {
 			return false
 		}
@@ -138,7 +139,7 @@ func TestOrderOfInt32(t *testing.T) {
 	check := func(values []int32) bool {
 		return checkOrdering(t, int32Order(values), bits.OrderOfInt32(values))
 	}
-	err := quickCheck(func(values []int32) bool {
+	err := quick.Check(func(values []int32) bool {
 		if !check(values) {
 			return false
 		}
@@ -176,7 +177,7 @@ func TestOrderOfInt64(t *testing.T) {
 	check := func(values []int64) bool {
 		return checkOrdering(t, int64Order(values), bits.OrderOfInt64(values))
 	}
-	err := quickCheck(func(values []int64) bool {
+	err := quick.Check(func(values []int64) bool {
 		if !check(values) {
 			return false
 		}
@@ -210,7 +211,7 @@ func TestOrderOfUint32(t *testing.T) {
 	check := func(values []uint32) bool {
 		return checkOrdering(t, uint32Order(values), bits.OrderOfUint32(values))
 	}
-	err := quickCheck(func(values []uint32) bool {
+	err := quick.Check(func(values []uint32) bool {
 		if !check(values) {
 			return false
 		}
@@ -244,7 +245,7 @@ func TestOrderOfUint64(t *testing.T) {
 	check := func(values []uint64) bool {
 		return checkOrdering(t, uint64Order(values), bits.OrderOfUint64(values))
 	}
-	err := quickCheck(func(values []uint64) bool {
+	err := quick.Check(func(values []uint64) bool {
 		if !check(values) {
 			return false
 		}
@@ -278,7 +279,7 @@ func TestOrderOfFloat32(t *testing.T) {
 	check := func(values []float32) bool {
 		return checkOrdering(t, float32Order(values), bits.OrderOfFloat32(values))
 	}
-	err := quickCheck(func(values []float32) bool {
+	err := quick.Check(func(values []float32) bool {
 		if !check(values) {
 			return false
 		}
@@ -312,7 +313,7 @@ func TestOrderOfFloat64(t *testing.T) {
 	check := func(values []float64) bool {
 		return checkOrdering(t, float64Order(values), bits.OrderOfFloat64(values))
 	}
-	err := quickCheck(func(values []float64) bool {
+	err := quick.Check(func(values []float64) bool {
 		if !check(values) {
 			return false
 		}
@@ -346,7 +347,7 @@ func TestOrderOfBytes(t *testing.T) {
 	check := func(values [][]byte) bool {
 		return checkOrdering(t, bytesOrder(values), bits.OrderOfBytes(values))
 	}
-	err := quickCheck(func(values [][16]byte) bool {
+	err := quick.Check(func(values [][16]byte) bool {
 		slices := make([][]byte, len(values))
 		for i := range values {
 			slices[i] = values[i][:]
diff --git a/internal/bits/sub_test.go b/internal/bits/sub_test.go
index ec038fc..caa71a1 100644
--- a/internal/bits/sub_test.go
+++ b/internal/bits/sub_test.go
@@ -5,10 +5,11 @@ import (
 	"testing"
 
 	"github.com/segmentio/parquet-go/internal/bits"
+	"github.com/segmentio/parquet-go/internal/quick"
 )
 
 func TestSubInt32(t *testing.T) {
-	err := quickCheck(func(values []int32) bool {
+	err := quick.Check(func(values []int32) bool {
 		const sub = math.MaxInt32
 
 		valuesAndSub := make([]int32, len(values))
@@ -32,7 +33,7 @@ func TestSubInt32(t *testing.T) {
 }
 
 func TestSubInt64(t *testing.T) {
-	err := quickCheck(func(values []int64) bool {
+	err := quick.Check(func(values []int64) bool {
 		const sub = math.MaxInt64
 
 		valuesAndSub := make([]int64, len(values))
diff --git a/parquet_test.go b/parquet_test.go
index 134af2e..1b4e5d7 100644
--- a/parquet_test.go
+++ b/parquet_test.go
@@ -13,11 +13,12 @@ import (
 	"github.com/google/uuid"
 	"github.com/segmentio/parquet-go"
 	"github.com/segmentio/parquet-go/deprecated"
+	"github.com/segmentio/parquet-go/internal/quick"
 )
 
 const (
 	benchmarkNumRows     = 10_000
-	benchmarkRowsPerStep = 100
+	benchmarkRowsPerStep = 1000
 )
 
 type benchmarkRowType struct {
@@ -31,6 +32,15 @@ func (row benchmarkRowType) generate(prng *rand.Rand) benchmarkRowType {
 	return row
 }
 
+type paddedBooleanColumn struct {
+	Value bool
+	_     [3]byte
+}
+
+func (row paddedBooleanColumn) generate(prng *rand.Rand) paddedBooleanColumn {
+	return paddedBooleanColumn{Value: prng.Int()%2 == 0}
+}
+
 type booleanColumn struct {
 	Value bool
 }
@@ -418,3 +428,14 @@ func generateString(r *rand.Rand, n int) string {
 	}
 	return b.String()
 }
+
+var quickCheckConfig = quick.Config{
+	Sizes: []int{
+		0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
+		10, 20, 30, 40, 50, 123,
+	},
+}
+
+func quickCheck(f interface{}) error {
+	return quickCheckConfig.Check(f)
+}
diff --git a/reader_go18_test.go b/reader_go18_test.go
index a48e927..0bbb66c 100644
--- a/reader_go18_test.go
+++ b/reader_go18_test.go
@@ -10,7 +10,6 @@ import (
 	"math/rand"
 	"reflect"
 	"testing"
-	"testing/quick"
 
 	"github.com/segmentio/parquet-go"
 )
@@ -40,10 +39,9 @@ func TestGenericReader(t *testing.T) {
 }
 
 func testGenericReader[Row any](t *testing.T) {
-	var prng = rand.New(rand.NewSource(2))
 	var model Row
 	t.Run(reflect.TypeOf(model).Name(), func(t *testing.T) {
-		f := func(rows []Row) bool {
+		err := quickCheck(func(rows []Row) bool {
 			if len(rows) == 0 {
 				return true // TODO: fix support for parquet files with zero rows
 			}
@@ -52,8 +50,8 @@ func testGenericReader[Row any](t *testing.T) {
 				return false
 			}
 			return true
-		}
-		if err := quick.Check(f, &quick.Config{Rand: prng}); err != nil {
+		})
+		if err != nil {
 			t.Error(err)
 		}
 	})
diff --git a/reader_test.go b/reader_test.go
index a82c63a..78a5493 100644
--- a/reader_test.go
+++ b/reader_test.go
@@ -8,9 +8,9 @@ import (
 	"math/rand"
 	"reflect"
 	"testing"
-	"testing/quick"
 
 	"github.com/segmentio/parquet-go"
+	"github.com/segmentio/parquet-go/internal/quick"
 )
 
 func rowsOf(numRows int, model interface{}) rows {
@@ -21,11 +21,10 @@ func rowsOf(numRows int, model interface{}) rows {
 func randomRowsOf(prng *rand.Rand, numRows int, model interface{}) rows {
 	typ := reflect.TypeOf(model)
 	rows := make(rows, numRows)
+	makeValue := quick.MakeValueFuncOf(typ)
 	for i := range rows {
-		v, ok := quick.Value(typ, prng)
-		if !ok {
-			panic("cannot generate random value for test")
-		}
+		v := reflect.New(typ).Elem()
+		makeValue(v, prng)
 		rows[i] = v.Interface()
 	}
 	return rows
@@ -290,7 +289,7 @@ func TestReaderReadSubset(t *testing.T) {
 	type Point3D struct{ X, Y, Z int64 }
 	type Point2D struct{ X, Y int64 }
 
-	f := func(points3D []Point3D) bool {
+	err := quickCheck(func(points3D []Point3D) bool {
 		if len(points3D) == 0 {
 			return true
 		}
@@ -317,8 +316,8 @@ func TestReaderReadSubset(t *testing.T) {
 			}
 		}
 		return true
-	}
-	if err := quick.Check(f, nil); err != nil {
+	})
+	if err != nil {
 		t.Error(err)
 	}
 }
diff --git a/row_group_test.go b/row_group_test.go
index ba2245d..4474438 100644
--- a/row_group_test.go
+++ b/row_group_test.go
@@ -2,11 +2,9 @@ package parquet_test
 
 import (
 	"bytes"
-	"math/rand"
 	"reflect"
 	"sort"
 	"testing"
-	"testing/quick"
 
 	"github.com/segmentio/parquet-go"
 )
@@ -66,7 +64,7 @@ func TestSeekToRow(t *testing.T) {
 }
 
 func testSeekToRow(t *testing.T, newRowGroup func([]Person) parquet.RowGroup) {
-	f := func(people []Person) bool {
+	err := quickCheck(func(people []Person) bool {
 		if len(people) == 0 { // TODO: fix creation of empty parquet files
 			return true
 		}
@@ -97,10 +95,8 @@ func testSeekToRow(t *testing.T, newRowGroup func([]Person) parquet.RowGroup) {
 		}
 
 		return true
-	}
-	if err := quick.Check(f, &quick.Config{
-		Rand: rand.New(rand.NewSource(3)),
-	}); err != nil {
+	})
+	if err != nil {
 		t.Error(err)
 	}
 }
diff --git a/schema_test.go b/schema_test.go
index 3a3231e..ea2c34b 100644
--- a/schema_test.go
+++ b/schema_test.go
@@ -55,6 +55,17 @@ func TestSchemaOf(t *testing.T) {
 }`,
 		},
 
+		{
+			value: new(struct {
+				Short float32 `parquet:"short,split"`
+				Long  float64 `parquet:"long,split"`
+			}),
+			print: `message {
+	required float short;
+	required double long;
+}`,
+		},
+
 		{
 			value: new(struct {
 				Inner struct {
diff --git a/writer_test.go b/writer_test.go
index 7ea7e45..c8655e3 100644
--- a/writer_test.go
+++ b/writer_test.go
@@ -7,7 +7,6 @@ import (
 	"os/exec"
 	"strings"
 	"testing"
-	"testing/quick"
 
 	"github.com/google/uuid"
 	"github.com/hexops/gotextdiff"
@@ -515,7 +514,7 @@ func TestWriterGenerateBloomFilters(t *testing.T) {
 		LastName  utf8string `parquet:"last_name"`
 	}
 
-	f := func(rows []Person) bool {
+	err := quickCheck(func(rows []Person) bool {
 		if len(rows) == 0 { // TODO: support writing files with no rows
 			return true
 		}
@@ -570,9 +569,8 @@ func TestWriterGenerateBloomFilters(t *testing.T) {
 		}
 
 		return true
-	}
-
-	if err := quick.Check(f, nil); err != nil {
+	})
+	if err != nil {
 		t.Error(err)
 	}
 }