Repository: segmentio/parquet-go
Pull request #101 (2022-03-16 20:48:37)

split column and offset index implementations
This PR refactors the internal representation of column and offset indexes to be different types instead of being implemented as a single page index type.

I am doing this in preparation for further changes I am planning to do, which will reduce the surface of code that needs to be modified.


Patch:
diff --git a/column_buffer.go b/column_buffer.go
index b2bc179..e2b6160 100644
--- a/column_buffer.go
+++ b/column_buffer.go
@@ -690,11 +690,11 @@ func (col *byteArrayColumnBuffer) Clone() ColumnBuffer {
 func (col *byteArrayColumnBuffer) Type() Type { return col.typ }
 
 func (col *byteArrayColumnBuffer) ColumnIndex() ColumnIndex {
-	return byteArrayPageIndex{&col.byteArrayPage}
+	return byteArrayColumnIndex{&col.byteArrayPage}
 }
 
 func (col *byteArrayColumnBuffer) OffsetIndex() OffsetIndex {
-	return byteArrayPageIndex{&col.byteArrayPage}
+	return byteArrayOffsetIndex{&col.byteArrayPage}
 }
 
 func (col *byteArrayColumnBuffer) BloomFilter() BloomFilter { return nil }
@@ -804,11 +804,11 @@ func (col *fixedLenByteArrayColumnBuffer) Clone() ColumnBuffer {
 func (col *fixedLenByteArrayColumnBuffer) Type() Type { return col.typ }
 
 func (col *fixedLenByteArrayColumnBuffer) ColumnIndex() ColumnIndex {
-	return fixedLenByteArrayPageIndex{&col.fixedLenByteArrayPage}
+	return fixedLenByteArrayColumnIndex{&col.fixedLenByteArrayPage}
 }
 
 func (col *fixedLenByteArrayColumnBuffer) OffsetIndex() OffsetIndex {
-	return fixedLenByteArrayPageIndex{&col.fixedLenByteArrayPage}
+	return fixedLenByteArrayOffsetIndex{&col.fixedLenByteArrayPage}
 }
 
 func (col *fixedLenByteArrayColumnBuffer) BloomFilter() BloomFilter { return nil }
diff --git a/column_buffer_default.go b/column_buffer_default.go
index 05031df..1d390bd 100644
--- a/column_buffer_default.go
+++ b/column_buffer_default.go
@@ -49,9 +49,13 @@ func (col *booleanColumnBuffer) Clone() ColumnBuffer {
 
 func (col *booleanColumnBuffer) Type() Type { return col.typ }
 
-func (col *booleanColumnBuffer) ColumnIndex() ColumnIndex { return booleanPageIndex{&col.booleanPage} }
+func (col *booleanColumnBuffer) ColumnIndex() ColumnIndex {
+	return booleanColumnIndex{&col.booleanPage}
+}
 
-func (col *booleanColumnBuffer) OffsetIndex() OffsetIndex { return booleanPageIndex{&col.booleanPage} }
+func (col *booleanColumnBuffer) OffsetIndex() OffsetIndex {
+	return booleanOffsetIndex{&col.booleanPage}
+}
 
 func (col *booleanColumnBuffer) BloomFilter() BloomFilter { return nil }
 
@@ -142,9 +146,9 @@ func (col *int32ColumnBuffer) Clone() ColumnBuffer {
 
 func (col *int32ColumnBuffer) Type() Type { return col.typ }
 
-func (col *int32ColumnBuffer) ColumnIndex() ColumnIndex { return int32PageIndex{&col.int32Page} }
+func (col *int32ColumnBuffer) ColumnIndex() ColumnIndex { return int32ColumnIndex{&col.int32Page} }
 
-func (col *int32ColumnBuffer) OffsetIndex() OffsetIndex { return int32PageIndex{&col.int32Page} }
+func (col *int32ColumnBuffer) OffsetIndex() OffsetIndex { return int32OffsetIndex{&col.int32Page} }
 
 func (col *int32ColumnBuffer) BloomFilter() BloomFilter { return nil }
 
@@ -237,9 +241,9 @@ func (col *int64ColumnBuffer) Clone() ColumnBuffer {
 
 func (col *int64ColumnBuffer) Type() Type { return col.typ }
 
-func (col *int64ColumnBuffer) ColumnIndex() ColumnIndex { return int64PageIndex{&col.int64Page} }
+func (col *int64ColumnBuffer) ColumnIndex() ColumnIndex { return int64ColumnIndex{&col.int64Page} }
 
-func (col *int64ColumnBuffer) OffsetIndex() OffsetIndex { return int64PageIndex{&col.int64Page} }
+func (col *int64ColumnBuffer) OffsetIndex() OffsetIndex { return int64OffsetIndex{&col.int64Page} }
 
 func (col *int64ColumnBuffer) BloomFilter() BloomFilter { return nil }
 
@@ -332,9 +336,9 @@ func (col *int96ColumnBuffer) Clone() ColumnBuffer {
 
 func (col *int96ColumnBuffer) Type() Type { return col.typ }
 
-func (col *int96ColumnBuffer) ColumnIndex() ColumnIndex { return int96PageIndex{&col.int96Page} }
+func (col *int96ColumnBuffer) ColumnIndex() ColumnIndex { return int96ColumnIndex{&col.int96Page} }
 
-func (col *int96ColumnBuffer) OffsetIndex() OffsetIndex { return int96PageIndex{&col.int96Page} }
+func (col *int96ColumnBuffer) OffsetIndex() OffsetIndex { return int96OffsetIndex{&col.int96Page} }
 
 func (col *int96ColumnBuffer) BloomFilter() BloomFilter { return nil }
 
@@ -427,9 +431,9 @@ func (col *floatColumnBuffer) Clone() ColumnBuffer {
 
 func (col *floatColumnBuffer) Type() Type { return col.typ }
 
-func (col *floatColumnBuffer) ColumnIndex() ColumnIndex { return floatPageIndex{&col.floatPage} }
+func (col *floatColumnBuffer) ColumnIndex() ColumnIndex { return floatColumnIndex{&col.floatPage} }
 
-func (col *floatColumnBuffer) OffsetIndex() OffsetIndex { return floatPageIndex{&col.floatPage} }
+func (col *floatColumnBuffer) OffsetIndex() OffsetIndex { return floatOffsetIndex{&col.floatPage} }
 
 func (col *floatColumnBuffer) BloomFilter() BloomFilter { return nil }
 
@@ -522,9 +526,9 @@ func (col *doubleColumnBuffer) Clone() ColumnBuffer {
 
 func (col *doubleColumnBuffer) Type() Type { return col.typ }
 
-func (col *doubleColumnBuffer) ColumnIndex() ColumnIndex { return doublePageIndex{&col.doublePage} }
+func (col *doubleColumnBuffer) ColumnIndex() ColumnIndex { return doubleColumnIndex{&col.doublePage} }
 
-func (col *doubleColumnBuffer) OffsetIndex() OffsetIndex { return doublePageIndex{&col.doublePage} }
+func (col *doubleColumnBuffer) OffsetIndex() OffsetIndex { return doubleOffsetIndex{&col.doublePage} }
 
 func (col *doubleColumnBuffer) BloomFilter() BloomFilter { return nil }
 
@@ -596,9 +600,9 @@ func newUint32ColumnBuffer(typ Type, columnIndex int16, bufferSize int) uint32Co
 	return uint32ColumnBuffer{newInt32ColumnBuffer(typ, columnIndex, bufferSize)}
 }
 
-func (col uint32ColumnBuffer) ColumnIndex() ColumnIndex { return uint32PageIndex{col.page()} }
+func (col uint32ColumnBuffer) ColumnIndex() ColumnIndex { return uint32ColumnIndex{col.page()} }
 
-func (col uint32ColumnBuffer) OffsetIndex() OffsetIndex { return uint32PageIndex{col.page()} }
+func (col uint32ColumnBuffer) OffsetIndex() OffsetIndex { return uint32OffsetIndex{col.page()} }
 
 func (col uint32ColumnBuffer) page() uint32Page { return uint32Page{&col.int32Page} }
 
@@ -620,9 +624,9 @@ func newUint64ColumnBuffer(typ Type, columnIndex int16, bufferSize int) uint64Co
 	return uint64ColumnBuffer{newInt64ColumnBuffer(typ, columnIndex, bufferSize)}
 }
 
-func (col uint64ColumnBuffer) ColumnIndex() ColumnIndex { return uint64PageIndex{col.page()} }
+func (col uint64ColumnBuffer) ColumnIndex() ColumnIndex { return uint64ColumnIndex{col.page()} }
 
-func (col uint64ColumnBuffer) OffsetIndex() OffsetIndex { return uint64PageIndex{col.page()} }
+func (col uint64ColumnBuffer) OffsetIndex() OffsetIndex { return uint64OffsetIndex{col.page()} }
 
 func (col uint64ColumnBuffer) page() uint64Page { return uint64Page{&col.int64Page} }
 
diff --git a/column_buffer_go18.go b/column_buffer_go18.go
index 8dfc8af..86f3a72 100644
--- a/column_buffer_go18.go
+++ b/column_buffer_go18.go
@@ -71,9 +71,9 @@ func (col *columnBuffer[T]) Clone() ColumnBuffer {
 
 func (col *columnBuffer[T]) Type() Type { return col.typ }
 
-func (col *columnBuffer[T]) ColumnIndex() ColumnIndex { return pageIndex[T]{&col.page} }
+func (col *columnBuffer[T]) ColumnIndex() ColumnIndex { return columnIndex[T]{&col.page} }
 
-func (col *columnBuffer[T]) OffsetIndex() OffsetIndex { return pageIndex[T]{&col.page} }
+func (col *columnBuffer[T]) OffsetIndex() OffsetIndex { return offsetIndex[T]{&col.page} }
 
 func (col *columnBuffer[T]) BloomFilter() BloomFilter { return nil }
 
diff --git a/column_index.go b/column_index.go
index d1c4867..d23387b 100644
--- a/column_index.go
+++ b/column_index.go
@@ -20,8 +20,8 @@ type ColumnIndex interface {
 
 	// PageIndex return min/max bounds for the page at the given index in the
 	// column.
-	MinValue(int) []byte
-	MaxValue(int) []byte
+	MinValue(int) Value
+	MaxValue(int) Value
 
 	// IsAscending returns true if the column index min/max values are sorted
 	// in ascending order (based on the ordering rules of the column's logical
@@ -34,35 +34,56 @@ type ColumnIndex interface {
 	IsDescending() bool
 }
 
-type columnIndex format.ColumnIndex
-
-func (i *columnIndex) NumPages() int         { return len(i.NullPages) }
-func (i *columnIndex) NullCount(j int) int64 { return i.NullCounts[j] }
-func (i *columnIndex) NullPage(j int) bool   { return i.NullPages[j] }
-func (i *columnIndex) MinValue(j int) []byte { return i.MinValues[j] }
-func (i *columnIndex) MaxValue(j int) []byte { return i.MaxValues[j] }
-func (i *columnIndex) IsAscending() bool     { return i.BoundaryOrder == format.Ascending }
-func (i *columnIndex) IsDescending() bool    { return i.BoundaryOrder == format.Descending }
-
-type byteArrayPageIndex struct{ page *byteArrayPage }
-
-func (i byteArrayPageIndex) NumPages() int       { return 1 }
-func (i byteArrayPageIndex) NullCount(int) int64 { return 0 }
-func (i byteArrayPageIndex) NullPage(int) bool   { return false }
-func (i byteArrayPageIndex) MinValue(int) []byte { return copyBytes(i.page.min()) }
-func (i byteArrayPageIndex) MaxValue(int) []byte { return copyBytes(i.page.max()) }
-func (i byteArrayPageIndex) IsAscending() bool   { return bytes.Compare(i.page.bounds()) < 0 }
-func (i byteArrayPageIndex) IsDescending() bool  { return bytes.Compare(i.page.bounds()) > 0 }
-
-type fixedLenByteArrayPageIndex struct{ page *fixedLenByteArrayPage }
-
-func (i fixedLenByteArrayPageIndex) NumPages() int       { return 1 }
-func (i fixedLenByteArrayPageIndex) NullCount(int) int64 { return 0 }
-func (i fixedLenByteArrayPageIndex) NullPage(int) bool   { return false }
-func (i fixedLenByteArrayPageIndex) MinValue(int) []byte { return copyBytes(i.page.min()) }
-func (i fixedLenByteArrayPageIndex) MaxValue(int) []byte { return copyBytes(i.page.max()) }
-func (i fixedLenByteArrayPageIndex) IsAscending() bool   { return bytes.Compare(i.page.bounds()) < 0 }
-func (i fixedLenByteArrayPageIndex) IsDescending() bool  { return bytes.Compare(i.page.bounds()) > 0 }
+type emptyColumnIndex struct{}
+
+func (emptyColumnIndex) NumPages() int       { return 0 }
+func (emptyColumnIndex) NullCount(int) int64 { return 0 }
+func (emptyColumnIndex) NullPage(int) bool   { return false }
+func (emptyColumnIndex) MinValue(int) Value  { return Value{} }
+func (emptyColumnIndex) MaxValue(int) Value  { return Value{} }
+func (emptyColumnIndex) IsAscending() bool   { return false }
+func (emptyColumnIndex) IsDescending() bool  { return false }
+
+type fileColumnIndex struct{ chunk *fileColumnChunk }
+
+func (i fileColumnIndex) NumPages() int         { return len(i.chunk.columnIndex.NullPages) }
+func (i fileColumnIndex) NullCount(j int) int64 { return i.chunk.columnIndex.NullCounts[j] }
+func (i fileColumnIndex) NullPage(j int) bool   { return i.chunk.columnIndex.NullPages[j] }
+func (i fileColumnIndex) MinValue(j int) Value  { return i.makeValue(i.chunk.columnIndex.MinValues[j]) }
+func (i fileColumnIndex) MaxValue(j int) Value  { return i.makeValue(i.chunk.columnIndex.MaxValues[j]) }
+func (i fileColumnIndex) IsAscending() bool {
+	return i.chunk.columnIndex.BoundaryOrder == format.Ascending
+}
+func (i fileColumnIndex) IsDescending() bool {
+	return i.chunk.columnIndex.BoundaryOrder == format.Descending
+}
+func (i *fileColumnIndex) makeValue(b []byte) Value {
+	return i.chunk.column.typ.Kind().Value(b)
+}
+
+type byteArrayColumnIndex struct{ page *byteArrayPage }
+
+func (i byteArrayColumnIndex) NumPages() int       { return 1 }
+func (i byteArrayColumnIndex) NullCount(int) int64 { return 0 }
+func (i byteArrayColumnIndex) NullPage(int) bool   { return false }
+func (i byteArrayColumnIndex) MinValue(int) Value  { return makeValueBytes(ByteArray, i.page.min()) }
+func (i byteArrayColumnIndex) MaxValue(int) Value  { return makeValueBytes(ByteArray, i.page.max()) }
+func (i byteArrayColumnIndex) IsAscending() bool   { return bytes.Compare(i.page.bounds()) < 0 }
+func (i byteArrayColumnIndex) IsDescending() bool  { return bytes.Compare(i.page.bounds()) > 0 }
+
+type fixedLenByteArrayColumnIndex struct{ page *fixedLenByteArrayPage }
+
+func (i fixedLenByteArrayColumnIndex) NumPages() int       { return 1 }
+func (i fixedLenByteArrayColumnIndex) NullCount(int) int64 { return 0 }
+func (i fixedLenByteArrayColumnIndex) NullPage(int) bool   { return false }
+func (i fixedLenByteArrayColumnIndex) MinValue(int) Value {
+	return makeValueBytes(FixedLenByteArray, i.page.min())
+}
+func (i fixedLenByteArrayColumnIndex) MaxValue(int) Value {
+	return makeValueBytes(FixedLenByteArray, i.page.max())
+}
+func (i fixedLenByteArrayColumnIndex) IsAscending() bool  { return bytes.Compare(i.page.bounds()) < 0 }
+func (i fixedLenByteArrayColumnIndex) IsDescending() bool { return bytes.Compare(i.page.bounds()) > 0 }
 
 // The ColumnIndexer interface is implemented by types that support generating
 // parquet column indexes.
diff --git a/column_index_default.go b/column_index_default.go
index 9ce976c..d9bace2 100644
--- a/column_index_default.go
+++ b/column_index_default.go
@@ -4,90 +4,89 @@ package parquet
 
 import (
 	"github.com/segmentio/parquet-go/deprecated"
-	"github.com/segmentio/parquet-go/encoding/plain"
 	"github.com/segmentio/parquet-go/format"
 	"github.com/segmentio/parquet-go/internal/bits"
 )
 
-type booleanPageIndex struct{ page *booleanPage }
-
-func (index booleanPageIndex) NumPages() int       { return 1 }
-func (index booleanPageIndex) NullCount(int) int64 { return 0 }
-func (index booleanPageIndex) NullPage(int) bool   { return false }
-func (index booleanPageIndex) MinValue(int) []byte { return plain.Boolean(index.page.min()) }
-func (index booleanPageIndex) MaxValue(int) []byte { return plain.Boolean(index.page.max()) }
-func (index booleanPageIndex) IsAscending() bool   { return compareBool(index.page.bounds()) < 0 }
-func (index booleanPageIndex) IsDescending() bool  { return compareBool(index.page.bounds()) > 0 }
-
-type int32PageIndex struct{ page *int32Page }
-
-func (index int32PageIndex) NumPages() int       { return 1 }
-func (index int32PageIndex) NullCount(int) int64 { return 0 }
-func (index int32PageIndex) NullPage(int) bool   { return false }
-func (index int32PageIndex) MinValue(int) []byte { return plain.Int32(index.page.min()) }
-func (index int32PageIndex) MaxValue(int) []byte { return plain.Int32(index.page.max()) }
-func (index int32PageIndex) IsAscending() bool   { return compareInt32(index.page.bounds()) < 0 }
-func (index int32PageIndex) IsDescending() bool  { return compareInt32(index.page.bounds()) > 0 }
-
-type int64PageIndex struct{ page *int64Page }
-
-func (index int64PageIndex) NumPages() int       { return 1 }
-func (index int64PageIndex) NullCount(int) int64 { return 0 }
-func (index int64PageIndex) NullPage(int) bool   { return false }
-func (index int64PageIndex) MinValue(int) []byte { return plain.Int64(index.page.min()) }
-func (index int64PageIndex) MaxValue(int) []byte { return plain.Int64(index.page.max()) }
-func (index int64PageIndex) IsAscending() bool   { return compareInt64(index.page.bounds()) < 0 }
-func (index int64PageIndex) IsDescending() bool  { return compareInt64(index.page.bounds()) > 0 }
-
-type int96PageIndex struct{ page *int96Page }
-
-func (index int96PageIndex) NumPages() int       { return 1 }
-func (index int96PageIndex) NullCount(int) int64 { return 0 }
-func (index int96PageIndex) NullPage(int) bool   { return false }
-func (index int96PageIndex) MinValue(int) []byte { return plain.Int96(index.page.min()) }
-func (index int96PageIndex) MaxValue(int) []byte { return plain.Int96(index.page.max()) }
-func (index int96PageIndex) IsAscending() bool   { return compareInt96(index.page.bounds()) < 0 }
-func (index int96PageIndex) IsDescending() bool  { return compareInt96(index.page.bounds()) > 0 }
-
-type floatPageIndex struct{ page *floatPage }
-
-func (index floatPageIndex) NumPages() int       { return 1 }
-func (index floatPageIndex) NullCount(int) int64 { return 0 }
-func (index floatPageIndex) NullPage(int) bool   { return false }
-func (index floatPageIndex) MinValue(int) []byte { return plain.Float(index.page.min()) }
-func (index floatPageIndex) MaxValue(int) []byte { return plain.Float(index.page.max()) }
-func (index floatPageIndex) IsAscending() bool   { return compareFloat32(index.page.bounds()) < 0 }
-func (index floatPageIndex) IsDescending() bool  { return compareFloat32(index.page.bounds()) > 0 }
-
-type doublePageIndex struct{ page *doublePage }
-
-func (index doublePageIndex) NumPages() int       { return 1 }
-func (index doublePageIndex) NullCount(int) int64 { return 0 }
-func (index doublePageIndex) NullPage(int) bool   { return false }
-func (index doublePageIndex) MinValue(int) []byte { return plain.Double(index.page.min()) }
-func (index doublePageIndex) MaxValue(int) []byte { return plain.Double(index.page.max()) }
-func (index doublePageIndex) IsAscending() bool   { return compareFloat64(index.page.bounds()) < 0 }
-func (index doublePageIndex) IsDescending() bool  { return compareFloat64(index.page.bounds()) > 0 }
-
-type uint32PageIndex struct{ page uint32Page }
-
-func (index uint32PageIndex) NumPages() int       { return 1 }
-func (index uint32PageIndex) NullCount(int) int64 { return 0 }
-func (index uint32PageIndex) NullPage(int) bool   { return false }
-func (index uint32PageIndex) MinValue(int) []byte { return plain.Int32(int32(index.page.min())) }
-func (index uint32PageIndex) MaxValue(int) []byte { return plain.Int32(int32(index.page.max())) }
-func (index uint32PageIndex) IsAscending() bool   { return compareUint32(index.page.bounds()) < 0 }
-func (index uint32PageIndex) IsDescending() bool  { return compareUint32(index.page.bounds()) > 0 }
-
-type uint64PageIndex struct{ page uint64Page }
-
-func (index uint64PageIndex) NumPages() int       { return 1 }
-func (index uint64PageIndex) NullCount(int) int64 { return 0 }
-func (index uint64PageIndex) NullPage(int) bool   { return false }
-func (index uint64PageIndex) MinValue(int) []byte { return plain.Int64(int64(index.page.min())) }
-func (index uint64PageIndex) MaxValue(int) []byte { return plain.Int64(int64(index.page.max())) }
-func (index uint64PageIndex) IsAscending() bool   { return compareUint64(index.page.bounds()) < 0 }
-func (index uint64PageIndex) IsDescending() bool  { return compareUint64(index.page.bounds()) > 0 }
+type booleanColumnIndex struct{ page *booleanPage }
+
+func (i booleanColumnIndex) NumPages() int       { return 1 }
+func (i booleanColumnIndex) NullCount(int) int64 { return 0 }
+func (i booleanColumnIndex) NullPage(int) bool   { return false }
+func (i booleanColumnIndex) MinValue(int) Value  { return makeValueBoolean(i.page.min()) }
+func (i booleanColumnIndex) MaxValue(int) Value  { return makeValueBoolean(i.page.max()) }
+func (i booleanColumnIndex) IsAscending() bool   { return compareBool(i.page.bounds()) < 0 }
+func (i booleanColumnIndex) IsDescending() bool  { return compareBool(i.page.bounds()) > 0 }
+
+type int32ColumnIndex struct{ page *int32Page }
+
+func (i int32ColumnIndex) NumPages() int       { return 1 }
+func (i int32ColumnIndex) NullCount(int) int64 { return 0 }
+func (i int32ColumnIndex) NullPage(int) bool   { return false }
+func (i int32ColumnIndex) MinValue(int) Value  { return makeValueInt32(i.page.min()) }
+func (i int32ColumnIndex) MaxValue(int) Value  { return makeValueInt32(i.page.max()) }
+func (i int32ColumnIndex) IsAscending() bool   { return compareInt32(i.page.bounds()) < 0 }
+func (i int32ColumnIndex) IsDescending() bool  { return compareInt32(i.page.bounds()) > 0 }
+
+type int64ColumnIndex struct{ page *int64Page }
+
+func (i int64ColumnIndex) NumPages() int       { return 1 }
+func (i int64ColumnIndex) NullCount(int) int64 { return 0 }
+func (i int64ColumnIndex) NullPage(int) bool   { return false }
+func (i int64ColumnIndex) MinValue(int) Value  { return makeValueInt64(i.page.min()) }
+func (i int64ColumnIndex) MaxValue(int) Value  { return makeValueInt64(i.page.max()) }
+func (i int64ColumnIndex) IsAscending() bool   { return compareInt64(i.page.bounds()) < 0 }
+func (i int64ColumnIndex) IsDescending() bool  { return compareInt64(i.page.bounds()) > 0 }
+
+type int96ColumnIndex struct{ page *int96Page }
+
+func (i int96ColumnIndex) NumPages() int       { return 1 }
+func (i int96ColumnIndex) NullCount(int) int64 { return 0 }
+func (i int96ColumnIndex) NullPage(int) bool   { return false }
+func (i int96ColumnIndex) MinValue(int) Value  { return makeValueInt96(i.page.min()) }
+func (i int96ColumnIndex) MaxValue(int) Value  { return makeValueInt96(i.page.max()) }
+func (i int96ColumnIndex) IsAscending() bool   { return compareInt96(i.page.bounds()) < 0 }
+func (i int96ColumnIndex) IsDescending() bool  { return compareInt96(i.page.bounds()) > 0 }
+
+type floatColumnIndex struct{ page *floatPage }
+
+func (i floatColumnIndex) NumPages() int       { return 1 }
+func (i floatColumnIndex) NullCount(int) int64 { return 0 }
+func (i floatColumnIndex) NullPage(int) bool   { return false }
+func (i floatColumnIndex) MinValue(int) Value  { return makeValueFloat(i.page.min()) }
+func (i floatColumnIndex) MaxValue(int) Value  { return makeValueFloat(i.page.max()) }
+func (i floatColumnIndex) IsAscending() bool   { return compareFloat32(i.page.bounds()) < 0 }
+func (i floatColumnIndex) IsDescending() bool  { return compareFloat32(i.page.bounds()) > 0 }
+
+type doubleColumnIndex struct{ page *doublePage }
+
+func (i doubleColumnIndex) NumPages() int       { return 1 }
+func (i doubleColumnIndex) NullCount(int) int64 { return 0 }
+func (i doubleColumnIndex) NullPage(int) bool   { return false }
+func (i doubleColumnIndex) MinValue(int) Value  { return makeValueDouble(i.page.min()) }
+func (i doubleColumnIndex) MaxValue(int) Value  { return makeValueDouble(i.page.max()) }
+func (i doubleColumnIndex) IsAscending() bool   { return compareFloat64(i.page.bounds()) < 0 }
+func (i doubleColumnIndex) IsDescending() bool  { return compareFloat64(i.page.bounds()) > 0 }
+
+type uint32ColumnIndex struct{ page uint32Page }
+
+func (i uint32ColumnIndex) NumPages() int       { return 1 }
+func (i uint32ColumnIndex) NullCount(int) int64 { return 0 }
+func (i uint32ColumnIndex) NullPage(int) bool   { return false }
+func (i uint32ColumnIndex) MinValue(int) Value  { return makeValueInt32(int32(i.page.min())) }
+func (i uint32ColumnIndex) MaxValue(int) Value  { return makeValueInt32(int32(i.page.max())) }
+func (i uint32ColumnIndex) IsAscending() bool   { return compareUint32(i.page.bounds()) < 0 }
+func (i uint32ColumnIndex) IsDescending() bool  { return compareUint32(i.page.bounds()) > 0 }
+
+type uint64ColumnIndex struct{ page uint64Page }
+
+func (i uint64ColumnIndex) NumPages() int       { return 1 }
+func (i uint64ColumnIndex) NullCount(int) int64 { return 0 }
+func (i uint64ColumnIndex) NullPage(int) bool   { return false }
+func (i uint64ColumnIndex) MinValue(int) Value  { return makeValueInt64(int64(i.page.min())) }
+func (i uint64ColumnIndex) MaxValue(int) Value  { return makeValueInt64(int64(i.page.max())) }
+func (i uint64ColumnIndex) IsAscending() bool   { return compareUint64(i.page.bounds()) < 0 }
+func (i uint64ColumnIndex) IsDescending() bool  { return compareUint64(i.page.bounds()) > 0 }
 
 type booleanColumnIndexer struct {
 	baseColumnIndexer
diff --git a/column_index_go18.go b/column_index_go18.go
index b7e326c..95cb2c3 100644
--- a/column_index_go18.go
+++ b/column_index_go18.go
@@ -8,23 +8,15 @@ import (
 	"github.com/segmentio/parquet-go/internal/cast"
 )
 
-type pageIndex[T primitive] struct{ page *page[T] }
-
-// ColumnIndex
-
-func (i pageIndex[T]) NumPages() int       { return 1 }
-func (i pageIndex[T]) NullCount(int) int64 { return 0 }
-func (i pageIndex[T]) NullPage(int) bool   { return false }
-func (i pageIndex[T]) MinValue(int) []byte { return i.page.class.plain(i.page.min()) }
-func (i pageIndex[T]) MaxValue(int) []byte { return i.page.class.plain(i.page.max()) }
-func (i pageIndex[T]) IsAscending() bool   { return i.page.class.compare(i.page.bounds()) < 0 }
-func (i pageIndex[T]) IsDescending() bool  { return i.page.class.compare(i.page.bounds()) > 0 }
-
-// OffsetIndex
-
-func (i pageIndex[T]) Offset(int) int64             { return 0 }
-func (i pageIndex[T]) CompressedPageSize(int) int64 { return i.page.Size() }
-func (i pageIndex[T]) FirstRowIndex(int) int64      { return 0 }
+type columnIndex[T primitive] struct{ page *page[T] }
+
+func (i columnIndex[T]) NumPages() int       { return 1 }
+func (i columnIndex[T]) NullCount(int) int64 { return 0 }
+func (i columnIndex[T]) NullPage(int) bool   { return false }
+func (i columnIndex[T]) MinValue(int) Value  { return i.page.class.makeValue(i.page.min()) }
+func (i columnIndex[T]) MaxValue(int) Value  { return i.page.class.makeValue(i.page.max()) }
+func (i columnIndex[T]) IsAscending() bool   { return i.page.class.compare(i.page.bounds()) < 0 }
+func (i columnIndex[T]) IsDescending() bool  { return i.page.class.compare(i.page.bounds()) > 0 }
 
 type columnIndexer[T primitive] struct {
 	class      *class[T]
diff --git a/convert.go b/convert.go
index 7dae066..dc99524 100644
--- a/convert.go
+++ b/convert.go
@@ -481,8 +481,8 @@ type missingColumnIndex struct{ *missingColumnChunk }
 func (i missingColumnIndex) NumPages() int       { return 1 }
 func (i missingColumnIndex) NullCount(int) int64 { return i.numNulls }
 func (i missingColumnIndex) NullPage(int) bool   { return true }
-func (i missingColumnIndex) MinValue(int) []byte { return nil }
-func (i missingColumnIndex) MaxValue(int) []byte { return nil }
+func (i missingColumnIndex) MinValue(int) Value  { return Value{} }
+func (i missingColumnIndex) MaxValue(int) Value  { return Value{} }
 func (i missingColumnIndex) IsAscending() bool   { return true }
 func (i missingColumnIndex) IsDescending() bool  { return false }
 
diff --git a/dictionary.go b/dictionary.go
index b9b0573..a3a49b7 100644
--- a/dictionary.go
+++ b/dictionary.go
@@ -943,13 +943,13 @@ type indexedColumnIndex struct{ col *indexedColumnBuffer }
 func (index indexedColumnIndex) NumPages() int       { return 1 }
 func (index indexedColumnIndex) NullCount(int) int64 { return 0 }
 func (index indexedColumnIndex) NullPage(int) bool   { return false }
-func (index indexedColumnIndex) MinValue(int) []byte {
+func (index indexedColumnIndex) MinValue(int) Value {
 	min, _ := index.col.Bounds()
-	return min.Bytes()
+	return min
 }
-func (index indexedColumnIndex) MaxValue(int) []byte {
+func (index indexedColumnIndex) MaxValue(int) Value {
 	_, max := index.col.Bounds()
-	return max.Bytes()
+	return max
 }
 func (index indexedColumnIndex) IsAscending() bool {
 	return index.col.typ.Compare(index.col.Bounds()) < 0
diff --git a/file.go b/file.go
index 85e1ee5..7035911 100644
--- a/file.go
+++ b/file.go
@@ -423,14 +423,14 @@ func (c *fileColumnChunk) ColumnIndex() ColumnIndex {
 	if c.columnIndex == nil {
 		return nil
 	}
-	return (*columnIndex)(c.columnIndex)
+	return fileColumnIndex{c}
 }
 
 func (c *fileColumnChunk) OffsetIndex() OffsetIndex {
 	if c.offsetIndex == nil {
 		return nil
 	}
-	return (*offsetIndex)(c.offsetIndex)
+	return (*fileOffsetIndex)(c.offsetIndex)
 }
 
 func (c *fileColumnChunk) BloomFilter() BloomFilter {
diff --git a/offset_index.go b/offset_index.go
index f3eb0d5..a9e5411 100644
--- a/offset_index.go
+++ b/offset_index.go
@@ -23,29 +23,41 @@ type OffsetIndex interface {
 	FirstRowIndex(int) int64
 }
 
-// OffsetIndex is the data structure representing offset indexes.
-type offsetIndex format.OffsetIndex
+type emptyOffsetIndex struct{}
 
-func (index *offsetIndex) NumPages() int {
-	return len(index.PageLocations)
+func (emptyOffsetIndex) NumPages() int                { return 0 }
+func (emptyOffsetIndex) Offset(int) int64             { return 0 }
+func (emptyOffsetIndex) CompressedPageSize(int) int64 { return 0 }
+func (emptyOffsetIndex) FirstRowIndex(int) int64      { return 0 }
+
+type fileOffsetIndex format.OffsetIndex
+
+func (i *fileOffsetIndex) NumPages() int {
+	return len(i.PageLocations)
 }
 
-func (index *offsetIndex) Offset(i int) int64 {
-	return index.PageLocations[i].Offset
+func (i *fileOffsetIndex) Offset(j int) int64 {
+	return i.PageLocations[j].Offset
 }
 
-func (index *offsetIndex) CompressedPageSize(i int) int64 {
-	return int64(index.PageLocations[i].CompressedPageSize)
+func (i *fileOffsetIndex) CompressedPageSize(j int) int64 {
+	return int64(i.PageLocations[j].CompressedPageSize)
 }
 
-func (index *offsetIndex) FirstRowIndex(i int) int64 {
-	return index.PageLocations[i].FirstRowIndex
+func (i *fileOffsetIndex) FirstRowIndex(j int) int64 {
+	return i.PageLocations[j].FirstRowIndex
 }
 
-func (index byteArrayPageIndex) Offset(int) int64             { return 0 }
-func (index byteArrayPageIndex) CompressedPageSize(int) int64 { return index.page.Size() }
-func (index byteArrayPageIndex) FirstRowIndex(int) int64      { return 0 }
+type byteArrayOffsetIndex struct{ page *byteArrayPage }
+
+func (i byteArrayOffsetIndex) NumPages() int                { return 1 }
+func (i byteArrayOffsetIndex) Offset(int) int64             { return 0 }
+func (i byteArrayOffsetIndex) CompressedPageSize(int) int64 { return i.page.Size() }
+func (i byteArrayOffsetIndex) FirstRowIndex(int) int64      { return 0 }
+
+type fixedLenByteArrayOffsetIndex struct{ page *fixedLenByteArrayPage }
 
-func (index fixedLenByteArrayPageIndex) Offset(int) int64             { return 0 }
-func (index fixedLenByteArrayPageIndex) CompressedPageSize(int) int64 { return index.page.Size() }
-func (index fixedLenByteArrayPageIndex) FirstRowIndex(int) int64      { return 0 }
+func (i fixedLenByteArrayOffsetIndex) NumPages() int                { return 1 }
+func (i fixedLenByteArrayOffsetIndex) Offset(int) int64             { return 0 }
+func (i fixedLenByteArrayOffsetIndex) CompressedPageSize(int) int64 { return i.page.Size() }
+func (i fixedLenByteArrayOffsetIndex) FirstRowIndex(int) int64      { return 0 }
diff --git a/offset_index_default.go b/offset_index_default.go
index db84e6b..71d5166 100644
--- a/offset_index_default.go
+++ b/offset_index_default.go
@@ -2,34 +2,58 @@
 
 package parquet
 
-func (index booleanPageIndex) Offset(int) int64             { return 0 }
-func (index booleanPageIndex) CompressedPageSize(int) int64 { return index.page.Size() }
-func (index booleanPageIndex) FirstRowIndex(int) int64      { return 0 }
+type booleanOffsetIndex struct{ page *booleanPage }
 
-func (index int32PageIndex) Offset(int) int64             { return 0 }
-func (index int32PageIndex) CompressedPageSize(int) int64 { return index.page.Size() }
-func (index int32PageIndex) FirstRowIndex(int) int64      { return 0 }
+func (i booleanOffsetIndex) NumPages() int                { return 1 }
+func (i booleanOffsetIndex) Offset(int) int64             { return 0 }
+func (i booleanOffsetIndex) CompressedPageSize(int) int64 { return i.page.Size() }
+func (i booleanOffsetIndex) FirstRowIndex(int) int64      { return 0 }
 
-func (index int64PageIndex) Offset(int) int64             { return 0 }
-func (index int64PageIndex) CompressedPageSize(int) int64 { return index.page.Size() }
-func (index int64PageIndex) FirstRowIndex(int) int64      { return 0 }
+type int32OffsetIndex struct{ page *int32Page }
 
-func (index int96PageIndex) Offset(int) int64             { return 0 }
-func (index int96PageIndex) CompressedPageSize(int) int64 { return index.page.Size() }
-func (index int96PageIndex) FirstRowIndex(int) int64      { return 0 }
+func (i int32OffsetIndex) NumPages() int                { return 1 }
+func (i int32OffsetIndex) Offset(int) int64             { return 0 }
+func (i int32OffsetIndex) CompressedPageSize(int) int64 { return i.page.Size() }
+func (i int32OffsetIndex) FirstRowIndex(int) int64      { return 0 }
 
-func (index floatPageIndex) Offset(int) int64             { return 0 }
-func (index floatPageIndex) CompressedPageSize(int) int64 { return index.page.Size() }
-func (index floatPageIndex) FirstRowIndex(int) int64      { return 0 }
+type int64OffsetIndex struct{ page *int64Page }
 
-func (index doublePageIndex) Offset(int) int64             { return 0 }
-func (index doublePageIndex) CompressedPageSize(int) int64 { return index.page.Size() }
-func (index doublePageIndex) FirstRowIndex(int) int64      { return 0 }
+func (i int64OffsetIndex) NumPages() int                { return 1 }
+func (i int64OffsetIndex) Offset(int) int64             { return 0 }
+func (i int64OffsetIndex) CompressedPageSize(int) int64 { return i.page.Size() }
+func (i int64OffsetIndex) FirstRowIndex(int) int64      { return 0 }
 
-func (index uint32PageIndex) Offset(int) int64             { return 0 }
-func (index uint32PageIndex) CompressedPageSize(int) int64 { return index.page.Size() }
-func (index uint32PageIndex) FirstRowIndex(int) int64      { return 0 }
+type int96OffsetIndex struct{ page *int96Page }
 
-func (index uint64PageIndex) Offset(int) int64             { return 0 }
-func (index uint64PageIndex) CompressedPageSize(int) int64 { return index.page.Size() }
-func (index uint64PageIndex) FirstRowIndex(int) int64      { return 0 }
+func (i int96OffsetIndex) NumPages() int                { return 1 }
+func (i int96OffsetIndex) Offset(int) int64             { return 0 }
+func (i int96OffsetIndex) CompressedPageSize(int) int64 { return i.page.Size() }
+func (i int96OffsetIndex) FirstRowIndex(int) int64      { return 0 }
+
+type floatOffsetIndex struct{ page *floatPage }
+
+func (i floatOffsetIndex) NumPages() int                { return 1 }
+func (i floatOffsetIndex) Offset(int) int64             { return 0 }
+func (i floatOffsetIndex) CompressedPageSize(int) int64 { return i.page.Size() }
+func (i floatOffsetIndex) FirstRowIndex(int) int64      { return 0 }
+
+type doubleOffsetIndex struct{ page *doublePage }
+
+func (i doubleOffsetIndex) NumPages() int                { return 1 }
+func (i doubleOffsetIndex) Offset(int) int64             { return 0 }
+func (i doubleOffsetIndex) CompressedPageSize(int) int64 { return i.page.Size() }
+func (i doubleOffsetIndex) FirstRowIndex(int) int64      { return 0 }
+
+type uint32OffsetIndex struct{ page uint32Page }
+
+func (i uint32OffsetIndex) NumPages() int                { return 1 }
+func (i uint32OffsetIndex) Offset(int) int64             { return 0 }
+func (i uint32OffsetIndex) CompressedPageSize(int) int64 { return i.page.Size() }
+func (i uint32OffsetIndex) FirstRowIndex(int) int64      { return 0 }
+
+type uint64OffsetIndex struct{ page uint64Page }
+
+func (i uint64OffsetIndex) NumPages() int                { return 1 }
+func (i uint64OffsetIndex) Offset(int) int64             { return 0 }
+func (i uint64OffsetIndex) CompressedPageSize(int) int64 { return i.page.Size() }
+func (i uint64OffsetIndex) FirstRowIndex(int) int64      { return 0 }
diff --git a/offset_index_go18.go b/offset_index_go18.go
new file mode 100644
index 0000000..8efdbdd
--- /dev/null
+++ b/offset_index_go18.go
@@ -0,0 +1,10 @@
+//go:build go1.18
+
+package parquet
+
+type offsetIndex[T primitive] struct{ page *page[T] }
+
+func (i offsetIndex[T]) NumPages() int                { return 1 }
+func (i offsetIndex[T]) Offset(int) int64             { return 0 }
+func (i offsetIndex[T]) CompressedPageSize(int) int64 { return i.page.Size() }
+func (i offsetIndex[T]) FirstRowIndex(int) int64      { return 0 }
diff --git a/row_group.go b/row_group.go
index 09be9c6..ab1baeb 100644
--- a/row_group.go
+++ b/row_group.go
@@ -449,8 +449,8 @@ type emptyColumnChunk struct {
 func (c *emptyColumnChunk) Type() Type               { return c.typ }
 func (c *emptyColumnChunk) Column() int              { return int(c.column) }
 func (c *emptyColumnChunk) Pages() Pages             { return emptyPages{} }
-func (c *emptyColumnChunk) ColumnIndex() ColumnIndex { return &emptyColumnIndex }
-func (c *emptyColumnChunk) OffsetIndex() OffsetIndex { return &emptyOffsetIndex }
+func (c *emptyColumnChunk) ColumnIndex() ColumnIndex { return emptyColumnIndex{} }
+func (c *emptyColumnChunk) OffsetIndex() OffsetIndex { return emptyOffsetIndex{} }
 func (c *emptyColumnChunk) BloomFilter() BloomFilter { return emptyBloomFilter{} }
 func (c *emptyColumnChunk) NumValues() int64         { return 0 }
 
@@ -473,9 +473,6 @@ func (emptyPages) ReadPage() (Page, error) { return nil, io.EOF }
 func (emptyPages) SeekToRow(int64) error   { return nil }
 
 var (
-	emptyColumnIndex = columnIndex{}
-	emptyOffsetIndex = offsetIndex{}
-
 	_ RowReaderWithSchema = (*rowGroupRowReader)(nil)
 	_ RowWriterTo         = (*rowGroupRowReader)(nil)
 
diff --git a/type.go b/type.go
index b95a53d..298f380 100644
--- a/type.go
+++ b/type.go
@@ -30,6 +30,18 @@ const (
 // String returns a human-readable representation of the physical type.
 func (k Kind) String() string { return format.Type(k).String() }
 
+// Value constructs a value form k and v.
+//
+// The method panics if the data is not a valid representation of the value
+// kind; for example, if the kind is Int32 but the data is not 4 bytes long.
+func (k Kind) Value(v []byte) Value {
+	x, err := parseValue(k, v)
+	if err != nil {
+		panic(err)
+	}
+	return x
+}
+
 // The Type interface represents logical types of the parquet type system.
 //
 // Types are immutable and therefore safe to access from multiple goroutines.
diff --git a/writer.go b/writer.go
index 1cea895..537089a 100644
--- a/writer.go
+++ b/writer.go
@@ -762,6 +762,12 @@ func (c *writerColumn) flushFilterPages() error {
 		if c.page.filter == nil {
 			c.page.filter = c.newBloomFilterEncoder(numValues)
 		}
+
+		// If there is a dictionary, we need to only write the dictionary.
+		if dict := c.dictionary; dict != nil {
+			return dict.WriteTo(c.page.filter)
+		}
+
 		for _, page := range c.filter {
 			if err := page.WriteTo(c.page.filter); err != nil {
 				return err


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 9a096cd..d25d6d8 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -9,6 +9,9 @@ jobs:
         go:
         - 1.17
         - 1.18
+        tags:
+        - ''
+        - purego
 
     runs-on: ubuntu-latest
 
@@ -27,4 +30,4 @@ jobs:
       run: test -z $(find . -name '*.go' | xargs gofmt -l)
 
     - name: Run Tests
-      run: go test -race ./...
+      run: go test -race -tags=${{ matrix.tags }} ./...
diff --git a/column_test.go b/column_test.go
index f726f67..ca5f63c 100644
--- a/column_test.go
+++ b/column_test.go
@@ -1,9 +1,7 @@
 package parquet_test
 
 import (
-	"bytes"
 	"fmt"
-	"reflect"
 	"testing"
 	"testing/quick"
 
@@ -145,16 +143,14 @@ func checkColumnChunkColumnIndex(columnChunk parquet.ColumnChunk) error {
 	numPages := columnIndex.NumPages()
 	pagesRead := 0
 	err := forEachPage(columnChunk.Pages(), func(page parquet.Page) error {
-		min, max := page.Bounds()
-		pageMin := min.Bytes()
-		pageMax := max.Bytes()
+		pageMin, pageMax := page.Bounds()
 		indexMin := columnIndex.MinValue(pagesRead)
 		indexMax := columnIndex.MaxValue(pagesRead)
 
-		if !bytes.Equal(pageMin, indexMin) {
+		if !parquet.Equal(pageMin, indexMin) {
 			return fmt.Errorf("max page value mismatch: index=%x page=%x", indexMin, pageMin)
 		}
-		if !bytes.Equal(pageMax, indexMax) {
+		if !parquet.Equal(pageMax, indexMax) {
 			return fmt.Errorf("max page value mismatch: index=%x page=%x", indexMax, pageMax)
 		}
 
@@ -183,12 +179,12 @@ func checkColumnChunkColumnIndex(columnChunk parquet.ColumnChunk) error {
 
 		switch {
 		case columnIndex.IsAscending():
-			if columnType.Compare(min, max) > 0 {
-				return fmt.Errorf("column index is declared to be in ascending order but %v > %v", min, max)
+			if columnType.Compare(pageMin, pageMax) > 0 {
+				return fmt.Errorf("column index is declared to be in ascending order but %v > %v", pageMin, pageMax)
 			}
 		case columnIndex.IsDescending():
-			if columnType.Compare(min, max) < 0 {
-				return fmt.Errorf("column index is declared to be in desending order but %v < %v", min, max)
+			if columnType.Compare(pageMin, pageMax) < 0 {
+				return fmt.Errorf("column index is declared to be in desending order but %v < %v", pageMin, pageMax)
 			}
 		}
 
@@ -278,9 +274,57 @@ func checkFileColumnIndex(f *parquet.File) error {
 		if i >= len(columnIndexes) {
 			return fmt.Errorf("more column indexes were read when iterating over column chunks than when reading from the file (i=%d,n=%d)", i, len(columnIndexes))
 		}
-		if !reflect.DeepEqual(&columnIndexes[i], newColumnIndex(columnIndex)) {
-			return fmt.Errorf("column index at index %d mismatch:\nfile  = %#v\nchunk = %#v", i, &columnIndexes[i], columnIndex)
+
+		index1 := columnIndex
+		index2 := &fileColumnIndex{
+			kind:        col.Type().Kind(),
+			ColumnIndex: columnIndexes[i],
+		}
+
+		numPages1 := index1.NumPages()
+		numPages2 := index2.NumPages()
+		if numPages1 != numPages2 {
+			return fmt.Errorf("number of pages mismatch: got=%d want=%d", numPages1, numPages2)
 		}
+
+		for j := 0; j < numPages1; j++ {
+			nullCount1 := index1.NullCount(j)
+			nullCount2 := index2.NullCount(j)
+			if nullCount1 != nullCount2 {
+				return fmt.Errorf("null count of page %d/%d mismatch: got=%d want=%d", i, numPages1, nullCount1, nullCount2)
+			}
+
+			nullPage1 := index1.NullPage(j)
+			nullPage2 := index2.NullPage(j)
+			if nullPage1 != nullPage2 {
+				return fmt.Errorf("null page of page %d/%d mismatch: got=%t want=%t", i, numPages1, nullPage1, nullPage2)
+			}
+
+			minValue1 := index1.MinValue(j)
+			minValue2 := index2.MinValue(j)
+			if !parquet.Equal(minValue1, minValue2) {
+				return fmt.Errorf("min value of page %d/%d mismatch: got=%v want=%v", i, numPages1, minValue1, minValue2)
+			}
+
+			maxValue1 := index1.MaxValue(j)
+			maxValue2 := index2.MaxValue(j)
+			if !parquet.Equal(maxValue1, maxValue2) {
+				return fmt.Errorf("max value of page %d/%d mismatch: got=%v want=%v", i, numPages1, maxValue1, maxValue2)
+			}
+
+			isAscending1 := index1.IsAscending()
+			isAscending2 := index2.IsAscending()
+			if isAscending1 != isAscending2 {
+				return fmt.Errorf("ascending state of page %d/%d mismatch: got=%t want=%t", i, numPages1, isAscending1, isAscending2)
+			}
+
+			isDescending1 := index1.IsDescending()
+			isDescending2 := index2.IsDescending()
+			if isDescending1 != isDescending2 {
+				return fmt.Errorf("descending state of page %d/%d mismatch: got=%t want=%t", i, numPages1, isDescending1, isDescending2)
+			}
+		}
+
 		i++
 		return nil
 	})
@@ -297,53 +341,59 @@ func checkFileOffsetIndex(f *parquet.File) error {
 		if i >= len(offsetIndexes) {
 			return fmt.Errorf("more offset indexes were read when iterating over column chunks than when reading from the file (i=%d,n=%d)", i, len(offsetIndexes))
 		}
-		if !reflect.DeepEqual(&offsetIndexes[i], newOffsetIndex(offsetIndex)) {
-			return fmt.Errorf("offset index at index %d mismatch:\nfile  = %#v\nchunk = %#v", i, &offsetIndexes[i], offsetIndex)
+
+		index1 := offsetIndex
+		index2 := (*fileOffsetIndex)(&offsetIndexes[i])
+
+		numPages1 := index1.NumPages()
+		numPages2 := index2.NumPages()
+		if numPages1 != numPages2 {
+			return fmt.Errorf("number of pages mismatch: got=%d want=%d", numPages1, numPages2)
+		}
+
+		for j := 0; j < numPages1; j++ {
+			offset1 := index1.Offset(j)
+			offset2 := index2.Offset(j)
+			if offset1 != offset2 {
+				return fmt.Errorf("offsets of page %d/%d mismatch: got=%d want=%d", i, numPages1, offset1, offset2)
+			}
+
+			compressedPageSize1 := index1.CompressedPageSize(j)
+			compressedPageSize2 := index2.CompressedPageSize(j)
+			if compressedPageSize1 != compressedPageSize2 {
+				return fmt.Errorf("compressed page size of page %d/%d mismatch: got=%d want=%d", i, numPages1, compressedPageSize1, compressedPageSize2)
+			}
+
+			firstRowIndex1 := index1.FirstRowIndex(j)
+			firstRowIndex2 := index2.FirstRowIndex(j)
+			if firstRowIndex1 != firstRowIndex2 {
+				return fmt.Errorf("first row index of page %d/%d mismatch: got=%d want=%d", i, numPages1, firstRowIndex1, firstRowIndex2)
+			}
 		}
+
 		i++
 		return nil
 	})
 }
 
-func newColumnIndex(columnIndex parquet.ColumnIndex) *format.ColumnIndex {
-	numPages := columnIndex.NumPages()
-
-	index := &format.ColumnIndex{
-		NullPages:  make([]bool, numPages),
-		MinValues:  make([][]byte, numPages),
-		MaxValues:  make([][]byte, numPages),
-		NullCounts: make([]int64, numPages),
-	}
-
-	for i := 0; i < numPages; i++ {
-		index.NullPages[i] = columnIndex.NullPage(i)
-		index.MinValues[i] = columnIndex.MinValue(i)
-		index.MaxValues[i] = columnIndex.MaxValue(i)
-		index.NullCounts[i] = columnIndex.NullCount(i)
-	}
-
-	switch {
-	case columnIndex.IsAscending():
-		index.BoundaryOrder = format.Ascending
-	case columnIndex.IsDescending():
-		index.BoundaryOrder = format.Descending
-	}
-
-	return index
+type fileColumnIndex struct {
+	kind parquet.Kind
+	format.ColumnIndex
 }
 
-func newOffsetIndex(offsetIndex parquet.OffsetIndex) *format.OffsetIndex {
-	index := &format.OffsetIndex{
-		PageLocations: make([]format.PageLocation, offsetIndex.NumPages()),
-	}
+func (i *fileColumnIndex) NumPages() int                { return len(i.NullPages) }
+func (i *fileColumnIndex) NullCount(j int) int64        { return i.NullCounts[j] }
+func (i *fileColumnIndex) NullPage(j int) bool          { return i.NullPages[j] }
+func (i *fileColumnIndex) MinValue(j int) parquet.Value { return i.kind.Value(i.MinValues[j]) }
+func (i *fileColumnIndex) MaxValue(j int) parquet.Value { return i.kind.Value(i.MaxValues[j]) }
+func (i *fileColumnIndex) IsAscending() bool            { return i.BoundaryOrder == format.Ascending }
+func (i *fileColumnIndex) IsDescending() bool           { return i.BoundaryOrder == format.Descending }
 
-	for i := range index.PageLocations {
-		index.PageLocations[i] = format.PageLocation{
-			Offset:             offsetIndex.Offset(i),
-			CompressedPageSize: int32(offsetIndex.CompressedPageSize(i)),
-			FirstRowIndex:      offsetIndex.FirstRowIndex(i),
-		}
-	}
+type fileOffsetIndex format.OffsetIndex
 
-	return index
+func (i *fileOffsetIndex) NumPages() int      { return len(i.PageLocations) }
+func (i *fileOffsetIndex) Offset(j int) int64 { return i.PageLocations[j].Offset }
+func (i *fileOffsetIndex) CompressedPageSize(j int) int64 {
+	return int64(i.PageLocations[j].CompressedPageSize)
 }
+func (i *fileOffsetIndex) FirstRowIndex(j int) int64 { return i.PageLocations[j].FirstRowIndex }
diff --git a/writer_test.go b/writer_test.go
index b4ab7a2..470e4c4 100644
--- a/writer_test.go
+++ b/writer_test.go
@@ -527,3 +527,41 @@ func TestWriterGenerateBloomFilters(t *testing.T) {
 		t.Error(err)
 	}
 }
+
+func TestBloomFilterForDict(t *testing.T) {
+	type testStruct struct {
+		A string `parquet:"a,dict"`
+	}
+
+	schema := parquet.SchemaOf(&testStruct{})
+
+	b := bytes.NewBuffer(nil)
+	w := parquet.NewWriter(
+		b,
+		schema,
+		parquet.BloomFilters(parquet.SplitBlockFilter("a")),
+	)
+
+	err := w.Write(&testStruct{A: "test"})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	err = w.Close()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	f, err := parquet.OpenFile(bytes.NewReader(b.Bytes()), int64(b.Len()))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	ok, err := f.RowGroup(0).Column(0).BloomFilter().Check(parquet.ValueOf("test"))
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !ok {
+		t.Error("bloom filter should have contained 'test'")
+	}
+}