Repository: segmentio/parquet-go
Pull request #68 (2022-02-16 18:56:43)

Increase column limit
Following up from our discussion with the Tempo and Parca folks, the limit on 127 columns appeared to be a bit too low for some use cases.

This PR modifies the package to support parquet schemas with up to 32K columns, which should be plenty for any practical use case of parquet.

I also made changes to the public API to always accept and return `int` values for repetition levels, definition levels, and column indexes, to give us the liberty to modify internal limits in the future without breaking code that depend on parquet-go. Finally, I added checks at API boundaries to ensure that conversions from `int` to smaller types do not cause overflows, and trigger panics with descriptive error messages if they do.

Let me know if you have any questions!


Patch:
diff --git a/README.md b/README.md
index 5a80406..a80e3de 100644
--- a/README.md
+++ b/README.md
@@ -236,7 +236,7 @@ buffer.Write(&Character{FirstName: "Anakin", LastName: "Skywalker"})
 sort.Sort(buffer)
 
 writer := parquet.NewWriter(output)
-_, err := parquet.CopyRows(writer, buffer)
+_, err := parquet.CopyRows(writer, buffer.Rows())
 if err != nil {
     ...
 }
diff --git a/buffer.go b/buffer.go
index a497ee0..8698881 100644
--- a/buffer.go
+++ b/buffer.go
@@ -70,7 +70,7 @@ func (buf *Buffer) configure(schema *Schema) {
 			columnType = dictionary.Type()
 		}
 
-		column := columnType.NewColumnBuffer(leaf.columnIndex, bufferSize)
+		column := columnType.NewColumnBuffer(int(leaf.columnIndex), bufferSize)
 		switch {
 		case leaf.maxRepetitionLevel > 0:
 			column = newRepeatedColumnBuffer(column, leaf.maxRepetitionLevel, leaf.maxDefinitionLevel, nullOrdering)
diff --git a/column.go b/column.go
index c4e4cd0..5753055 100644
--- a/column.go
+++ b/column.go
@@ -35,7 +35,7 @@ type Column struct {
 	depth              int8
 	maxRepetitionLevel int8
 	maxDefinitionLevel int8
-	index              int8
+	index              int16
 }
 
 // Type returns the type of the column.
@@ -174,19 +174,19 @@ func (r *columnPages) SeekToRow(rowIndex int64) error {
 }
 
 // Depth returns the position of the column relative to the root.
-func (c *Column) Depth() int8 { return c.depth }
+func (c *Column) Depth() int { return int(c.depth) }
 
 // MaxRepetitionLevel returns the maximum value of repetition levels on this
 // column.
-func (c *Column) MaxRepetitionLevel() int8 { return c.maxRepetitionLevel }
+func (c *Column) MaxRepetitionLevel() int { return int(c.maxRepetitionLevel) }
 
 // MaxDefinitionLevel returns the maximum value of definition levels on this
 // column.
-func (c *Column) MaxDefinitionLevel() int8 { return c.maxDefinitionLevel }
+func (c *Column) MaxDefinitionLevel() int { return int(c.maxDefinitionLevel) }
 
 // Index returns the position of the column in a row. Only leaf columns have a
 // column index, the method returns -1 when called on non-leaf columns.
-func (c *Column) Index() int8 { return c.index }
+func (c *Column) Index() int { return int(c.index) }
 
 // ValueByName returns the sub-value with the given name in base.
 func (c *Column) ValueByName(base reflect.Value, name string) reflect.Value {
@@ -270,7 +270,7 @@ func (c *Column) setLevels(depth, repetition, definition, index int) (int, error
 	if len(c.columns) > 0 {
 		c.index = -1
 	} else {
-		c.index = int8(index)
+		c.index = int16(index)
 		index++
 	}
 
diff --git a/column_buffer.go b/column_buffer.go
index 443ba01..88f7341 100644
--- a/column_buffer.go
+++ b/column_buffer.go
@@ -455,10 +455,10 @@ func (col *repeatedColumnBuffer) Page() BufferedPage {
 			for i := int64(0); i < numValues; i++ {
 				var err error
 				if buffer, err = col.base.ReadRowAt(buffer[:0], int64(row.offset)+i); err != nil {
-					return newErrorPage(col.Column(), "reordering rows of repeated column: %w", err)
+					return newErrorPage(int16(col.Column()), "reordering rows of repeated column: %w", err)
 				}
 				if err = column.base.WriteRow(buffer); err != nil {
-					return newErrorPage(col.Column(), "reordering rows of repeated column: %w", err)
+					return newErrorPage(int16(col.Column()), "reordering rows of repeated column: %w", err)
 				}
 			}
 		}
@@ -681,11 +681,11 @@ type booleanColumnBuffer struct {
 	typ Type
 }
 
-func newBooleanColumnBuffer(typ Type, columnIndex, bufferSize int) *booleanColumnBuffer {
+func newBooleanColumnBuffer(typ Type, columnIndex int16, bufferSize int) *booleanColumnBuffer {
 	return &booleanColumnBuffer{
 		booleanPage: booleanPage{
 			values:      make([]bool, 0, bufferSize),
-			columnIndex: ^int8(columnIndex),
+			columnIndex: ^columnIndex,
 		},
 		typ: typ,
 	}
@@ -774,11 +774,11 @@ type int32ColumnBuffer struct {
 	typ Type
 }
 
-func newInt32ColumnBuffer(typ Type, columnIndex, bufferSize int) *int32ColumnBuffer {
+func newInt32ColumnBuffer(typ Type, columnIndex int16, bufferSize int) *int32ColumnBuffer {
 	return &int32ColumnBuffer{
 		int32Page: int32Page{
 			values:      make([]int32, 0, bufferSize/4),
-			columnIndex: ^int8(columnIndex),
+			columnIndex: ^columnIndex,
 		},
 		typ: typ,
 	}
@@ -869,11 +869,11 @@ type int64ColumnBuffer struct {
 	typ Type
 }
 
-func newInt64ColumnBuffer(typ Type, columnIndex, bufferSize int) *int64ColumnBuffer {
+func newInt64ColumnBuffer(typ Type, columnIndex int16, bufferSize int) *int64ColumnBuffer {
 	return &int64ColumnBuffer{
 		int64Page: int64Page{
 			values:      make([]int64, 0, bufferSize/8),
-			columnIndex: ^int8(columnIndex),
+			columnIndex: ^columnIndex,
 		},
 		typ: typ,
 	}
@@ -964,11 +964,11 @@ type int96ColumnBuffer struct {
 	typ Type
 }
 
-func newInt96ColumnBuffer(typ Type, columnIndex, bufferSize int) *int96ColumnBuffer {
+func newInt96ColumnBuffer(typ Type, columnIndex int16, bufferSize int) *int96ColumnBuffer {
 	return &int96ColumnBuffer{
 		int96Page: int96Page{
 			values:      make([]deprecated.Int96, 0, bufferSize/12),
-			columnIndex: ^int8(columnIndex),
+			columnIndex: ^columnIndex,
 		},
 		typ: typ,
 	}
@@ -1059,11 +1059,11 @@ type floatColumnBuffer struct {
 	typ Type
 }
 
-func newFloatColumnBuffer(typ Type, columnIndex, bufferSize int) *floatColumnBuffer {
+func newFloatColumnBuffer(typ Type, columnIndex int16, bufferSize int) *floatColumnBuffer {
 	return &floatColumnBuffer{
 		floatPage: floatPage{
 			values:      make([]float32, 0, bufferSize/4),
-			columnIndex: ^int8(columnIndex),
+			columnIndex: ^columnIndex,
 		},
 		typ: typ,
 	}
@@ -1154,11 +1154,11 @@ type doubleColumnBuffer struct {
 	typ Type
 }
 
-func newDoubleColumnBuffer(typ Type, columnIndex, bufferSize int) *doubleColumnBuffer {
+func newDoubleColumnBuffer(typ Type, columnIndex int16, bufferSize int) *doubleColumnBuffer {
 	return &doubleColumnBuffer{
 		doublePage: doublePage{
 			values:      make([]float64, 0, bufferSize/8),
-			columnIndex: ^int8(columnIndex),
+			columnIndex: ^columnIndex,
 		},
 		typ: typ,
 	}
@@ -1249,11 +1249,11 @@ type byteArrayColumnBuffer struct {
 	typ Type
 }
 
-func newByteArrayColumnBuffer(typ Type, columnIndex, bufferSize int) *byteArrayColumnBuffer {
+func newByteArrayColumnBuffer(typ Type, columnIndex int16, bufferSize int) *byteArrayColumnBuffer {
 	return &byteArrayColumnBuffer{
 		byteArrayPage: byteArrayPage{
 			values:      encoding.MakeByteArrayList(bufferSize / 16),
-			columnIndex: ^int8(columnIndex),
+			columnIndex: ^columnIndex,
 		},
 		typ: typ,
 	}
@@ -1354,13 +1354,13 @@ type fixedLenByteArrayColumnBuffer struct {
 	tmp []byte
 }
 
-func newFixedLenByteArrayColumnBuffer(typ Type, columnIndex, bufferSize int) *fixedLenByteArrayColumnBuffer {
+func newFixedLenByteArrayColumnBuffer(typ Type, columnIndex int16, bufferSize int) *fixedLenByteArrayColumnBuffer {
 	size := typ.Length()
 	return &fixedLenByteArrayColumnBuffer{
 		fixedLenByteArrayPage: fixedLenByteArrayPage{
 			size:        size,
 			data:        make([]byte, 0, bufferSize),
-			columnIndex: ^int8(columnIndex),
+			columnIndex: ^columnIndex,
 		},
 		typ: typ,
 		tmp: make([]byte, size),
@@ -1469,7 +1469,7 @@ func (col *fixedLenByteArrayColumnBuffer) ReadRowAt(row Row, index int64) (Row,
 
 type uint32ColumnBuffer struct{ *int32ColumnBuffer }
 
-func newUint32ColumnBuffer(typ Type, columnIndex, bufferSize int) uint32ColumnBuffer {
+func newUint32ColumnBuffer(typ Type, columnIndex int16, bufferSize int) uint32ColumnBuffer {
 	return uint32ColumnBuffer{newInt32ColumnBuffer(typ, columnIndex, bufferSize)}
 }
 
@@ -1493,7 +1493,7 @@ func (col uint32ColumnBuffer) Less(i, j int) bool {
 
 type uint64ColumnBuffer struct{ *int64ColumnBuffer }
 
-func newUint64ColumnBuffer(typ Type, columnIndex, bufferSize int) uint64ColumnBuffer {
+func newUint64ColumnBuffer(typ Type, columnIndex int16, bufferSize int) uint64ColumnBuffer {
 	return uint64ColumnBuffer{newInt64ColumnBuffer(typ, columnIndex, bufferSize)}
 }
 
diff --git a/column_path.go b/column_path.go
index 560b5f1..28ebc5b 100644
--- a/column_path.go
+++ b/column_path.go
@@ -53,16 +53,16 @@ func stringsAreOrdered(strings1, strings2 []string) bool {
 type leafColumn struct {
 	node               Node
 	path               columnPath
-	columnIndex        int
 	maxRepetitionLevel int8
 	maxDefinitionLevel int8
+	columnIndex        int16
 }
 
 func forEachLeafColumnOf(node Node, do func(leafColumn)) {
 	forEachLeafColumn(node, nil, 0, 0, 0, do)
 }
 
-func forEachLeafColumn(node Node, path columnPath, columnIndex int, maxRepetitionLevel, maxDefinitionLevel int8, do func(leafColumn)) int {
+func forEachLeafColumn(node Node, path columnPath, columnIndex, maxRepetitionLevel, maxDefinitionLevel int, do func(leafColumn)) int {
 	switch {
 	case node.Optional():
 		maxDefinitionLevel++
@@ -75,9 +75,9 @@ func forEachLeafColumn(node Node, path columnPath, columnIndex int, maxRepetitio
 		do(leafColumn{
 			node:               node,
 			path:               path,
-			columnIndex:        columnIndex,
-			maxRepetitionLevel: maxRepetitionLevel,
-			maxDefinitionLevel: maxDefinitionLevel,
+			maxRepetitionLevel: makeRepetitionLevel(maxRepetitionLevel),
+			maxDefinitionLevel: makeDefinitionLevel(maxDefinitionLevel),
+			columnIndex:        makeColumnIndex(columnIndex),
 		})
 		return columnIndex + 1
 	}
diff --git a/concat.go b/concat.go
index e5d3393..a35921e 100644
--- a/concat.go
+++ b/concat.go
@@ -13,7 +13,7 @@ func concat(schema *Schema, rowGroups []RowGroup) *concatenatedRowGroup {
 func (c *concatenatedRowGroup) init(schema *Schema, rowGroups []RowGroup) {
 	c.schema = schema
 	c.rowGroups = rowGroups
-	c.columns = make([]concatenatedColumnChunk, numColumnsOf(schema))
+	c.columns = make([]concatenatedColumnChunk, numLeafColumnsOf(schema))
 
 	for i := range c.columns {
 		c.columns[i].rowGroup = c
diff --git a/convert.go b/convert.go
index 9c3f7db..bd12201 100644
--- a/convert.go
+++ b/convert.go
@@ -38,7 +38,7 @@ type Conversion interface {
 
 type conversion struct {
 	convert convertFunc
-	columns []int
+	columns []int16
 	schema  *Schema
 }
 
@@ -50,7 +50,7 @@ func (c *conversion) Convert(dst, src Row) (Row, error) {
 	return dst, err
 }
 
-func (c *conversion) Column(i int) int { return c.columns[i] }
+func (c *conversion) Column(i int) int { return int(c.columns[i]) }
 
 func (c *conversion) Schema() *Schema { return c.schema }
 
@@ -74,7 +74,7 @@ func Convert(to, from Node) (conv Conversion, err error) {
 		}
 	}()
 
-	columns := make([]int, numColumnsOf(to))
+	columns := make([]int16, numLeafColumnsOf(to))
 	for i := range columns {
 		columns[i] = -1
 	}
@@ -94,7 +94,7 @@ func Convert(to, from Node) (conv Conversion, err error) {
 type convertFunc func(Row, Row, levels) (Row, Row, error)
 
 type convertNode struct {
-	columnIndex int
+	columnIndex int16
 	node        Node
 	path        columnPath
 }
@@ -105,7 +105,7 @@ func (c convertNode) child(name string) convertNode {
 	return c
 }
 
-func convert(to, from convertNode, columns []int) (int, int, convertFunc) {
+func convert(to, from convertNode, columns []int16) (int16, int16, convertFunc) {
 	switch {
 	case from.node.Optional():
 		if to.node.Optional() {
@@ -144,7 +144,7 @@ func convertError(to, from convertNode, reason string) *ConvertError {
 }
 
 //go:noinline
-func convertFuncOfOptional(to, from convertNode, columns []int) (int, int, convertFunc) {
+func convertFuncOfOptional(to, from convertNode, columns []int16) (int16, int16, convertFunc) {
 	to.node = Required(to.node)
 	from.node = Required(from.node)
 
@@ -156,10 +156,10 @@ func convertFuncOfOptional(to, from convertNode, columns []int) (int, int, conve
 }
 
 //go:noinline
-func convertFuncOfRepeated(to, from convertNode, columns []int) (int, int, convertFunc) {
+func convertFuncOfRepeated(to, from convertNode, columns []int16) (int16, int16, convertFunc) {
 	to.node = Required(to.node)
 	from.node = Required(from.node)
-	srcColumnIndex := ^int8(from.columnIndex)
+	srcColumnIndex := ^from.columnIndex
 
 	toColumnIndex, fromColumnIndex, conv := convert(to, from, columns)
 	return toColumnIndex, fromColumnIndex, func(dst, src Row, levels levels) (Row, Row, error) {
@@ -180,13 +180,13 @@ func convertFuncOfRepeated(to, from convertNode, columns []int) (int, int, conve
 }
 
 //go:noinline
-func convertFuncOfLeaf(to, from convertNode, columns []int) (int, int, convertFunc) {
+func convertFuncOfLeaf(to, from convertNode, columns []int16) (int16, int16, convertFunc) {
 	if !typesAreEqual(to.node, from.node) {
 		panic(convertError(to, from, fmt.Sprintf("unsupported type conversion from %s to %s for parquet column", from.node.Type(), to.node.Type())))
 	}
 
-	srcColumnIndex := ^int8(from.columnIndex)
-	dstColumnIndex := ^int8(to.columnIndex)
+	srcColumnIndex := ^from.columnIndex
+	dstColumnIndex := ^to.columnIndex
 	columns[to.columnIndex] = from.columnIndex
 
 	return to.columnIndex + 1, from.columnIndex + 1, func(dst, src Row, levels levels) (Row, Row, error) {
@@ -202,7 +202,7 @@ func convertFuncOfLeaf(to, from convertNode, columns []int) (int, int, convertFu
 }
 
 //go:noinline
-func convertFuncOfGroup(to, from convertNode, columns []int) (int, int, convertFunc) {
+func convertFuncOfGroup(to, from convertNode, columns []int16) (int16, int16, convertFunc) {
 	extra, missing, names := comm(to.node.ChildNames(), from.node.ChildNames())
 	funcs := make([]convertFunc, 0, len(extra)+len(missing)+len(names))
 
@@ -236,7 +236,7 @@ func makeGroupConvertFunc(funcs []convertFunc) convertFunc {
 	}
 }
 
-func convertFuncOfExtraColumn(to convertNode) (int, convertFunc) {
+func convertFuncOfExtraColumn(to convertNode) (int16, convertFunc) {
 	switch {
 	case isLeaf(to.node):
 		return convertFuncOfExtraLeaf(to)
@@ -246,12 +246,12 @@ func convertFuncOfExtraColumn(to convertNode) (int, convertFunc) {
 }
 
 //go:noinline
-func convertFuncOfExtraLeaf(to convertNode) (int, convertFunc) {
-	kind := ^int8(to.node.Type().Kind())
-	columnIndex := ^int8(to.columnIndex)
+func convertFuncOfExtraLeaf(to convertNode) (int16, convertFunc) {
+	kind := ^to.node.Type().Kind()
+	columnIndex := ^to.columnIndex
 	return to.columnIndex + 1, func(dst, src Row, levels levels) (Row, Row, error) {
 		dst = append(dst, Value{
-			kind:            kind,
+			kind:            int8(kind),
 			repetitionLevel: levels.repetitionLevel,
 			definitionLevel: levels.definitionLevel,
 			columnIndex:     columnIndex,
@@ -261,7 +261,7 @@ func convertFuncOfExtraLeaf(to convertNode) (int, convertFunc) {
 }
 
 //go:noinline
-func convertFuncOfExtraGroup(to convertNode) (int, convertFunc) {
+func convertFuncOfExtraGroup(to convertNode) (int16, convertFunc) {
 	names := to.node.ChildNames()
 	funcs := make([]convertFunc, len(names))
 	for i := range funcs {
@@ -271,12 +271,12 @@ func convertFuncOfExtraGroup(to convertNode) (int, convertFunc) {
 }
 
 //go:noinline
-func convertFuncOfMissingColumn(from convertNode) (int, convertFunc) {
-	rowLength := numColumnsOf(from.node)
-	columnIndex := ^int8(from.columnIndex)
+func convertFuncOfMissingColumn(from convertNode) (int16, convertFunc) {
+	rowLength := numLeafColumnsOf(from.node)
+	columnIndex := ^from.columnIndex
 	return from.columnIndex + rowLength, func(dst, src Row, levels levels) (Row, Row, error) {
 		for len(src) > 0 && src[0].columnIndex == columnIndex {
-			if len(src) < rowLength {
+			if len(src) < int(rowLength) {
 				break
 			}
 			src = src[rowLength:]
@@ -368,10 +368,10 @@ func ConvertRowGroup(rowGroup RowGroup, conv Conversion) RowGroup {
 	schema := conv.Schema()
 	numRows := rowGroup.NumRows()
 
-	columns := make([]ColumnChunk, numColumnsOf(schema))
+	columns := make([]ColumnChunk, numLeafColumnsOf(schema))
 	forEachLeafColumnOf(schema, func(leaf leafColumn) {
 		i := leaf.columnIndex
-		j := conv.Column(leaf.columnIndex)
+		j := conv.Column(int(leaf.columnIndex))
 		if j < 0 {
 			columns[i] = &missingColumnChunk{
 				typ:    leaf.node.Type(),
@@ -426,14 +426,14 @@ func ConvertRowGroup(rowGroup RowGroup, conv Conversion) RowGroup {
 
 type missingColumnChunk struct {
 	typ       Type
-	column    int
+	column    int16
 	numRows   int64
 	numValues int64
 	numNulls  int64
 }
 
 func (c *missingColumnChunk) Type() Type               { return c.typ }
-func (c *missingColumnChunk) Column() int              { return c.column }
+func (c *missingColumnChunk) Column() int              { return int(c.column) }
 func (c *missingColumnChunk) Pages() Pages             { return onePage(missingPage{c}) }
 func (c *missingColumnChunk) ColumnIndex() ColumnIndex { return missingColumnIndex{c} }
 func (c *missingColumnChunk) OffsetIndex() OffsetIndex { return missingOffsetIndex{} }
@@ -465,7 +465,7 @@ func (missingBloomFilter) Check([]byte) (bool, error)        { return false, nil
 
 type missingPage struct{ *missingColumnChunk }
 
-func (p missingPage) Column() int              { return p.column }
+func (p missingPage) Column() int              { return int(p.column) }
 func (p missingPage) Dictionary() Dictionary   { return nil }
 func (p missingPage) NumRows() int64           { return p.numRows }
 func (p missingPage) NumValues() int64         { return p.numValues }
@@ -489,7 +489,7 @@ func (r *missingValues) ReadValues(values []Value) (int, error) {
 	}
 	for i := range values {
 		// TODO: how do we set the repetition and definition levels here?
-		values[i] = Value{columnIndex: ^int8(r.page.column)}
+		values[i] = Value{columnIndex: ^r.page.column}
 	}
 	if r.read += int64(len(values)); r.read == r.page.numValues {
 		return len(values), io.EOF
diff --git a/dictionary.go b/dictionary.go
index fa9dba3..dfce3b3 100644
--- a/dictionary.go
+++ b/dictionary.go
@@ -684,7 +684,7 @@ func newIndexedType(typ Type, dict Dictionary) *indexedType {
 }
 
 func (t *indexedType) NewColumnBuffer(columnIndex, bufferSize int) ColumnBuffer {
-	return newIndexedColumnBuffer(t.dict, t, columnIndex, bufferSize)
+	return newIndexedColumnBuffer(t.dict, t, makeColumnIndex(columnIndex), bufferSize)
 }
 
 func (t *indexedType) NewValueDecoder(bufferSize int) ValueDecoder {
@@ -694,7 +694,7 @@ func (t *indexedType) NewValueDecoder(bufferSize int) ValueDecoder {
 type indexedPage struct {
 	dict        Dictionary
 	values      []int32
-	columnIndex int8
+	columnIndex int16
 }
 
 func (page *indexedPage) Column() int { return int(^page.columnIndex) }
@@ -778,12 +778,12 @@ type indexedColumnBuffer struct {
 	typ Type
 }
 
-func newIndexedColumnBuffer(dict Dictionary, typ Type, columnIndex, bufferSize int) *indexedColumnBuffer {
+func newIndexedColumnBuffer(dict Dictionary, typ Type, columnIndex int16, bufferSize int) *indexedColumnBuffer {
 	return &indexedColumnBuffer{
 		indexedPage: indexedPage{
 			dict:        dict,
 			values:      make([]int32, 0, bufferSize/4),
-			columnIndex: ^int8(columnIndex),
+			columnIndex: ^columnIndex,
 		},
 		typ: typ,
 	}
diff --git a/file.go b/file.go
index 7afef36..95c474b 100644
--- a/file.go
+++ b/file.go
@@ -813,7 +813,7 @@ func (p *filePage) Buffer() BufferedPage {
 	bufferedPage := p.column.Type().NewColumnBuffer(p.Column(), int(p.Size()))
 	_, err := CopyValues(bufferedPage, p.Values())
 	if err != nil {
-		return &errorPage{err: err, columnIndex: p.Column()}
+		return &errorPage{err: err, columnIndex: int16(p.Column())}
 	}
 	return bufferedPage.Page()
 }
@@ -926,9 +926,9 @@ func (s *filePageValueReaderState) init(columnType Type, column *Column, codec f
 	if s.reader == nil {
 		s.reader = newDataPageReader(
 			columnType,
-			column.MaxRepetitionLevel(),
-			column.MaxDefinitionLevel(),
-			column.Index(),
+			column.maxRepetitionLevel,
+			column.maxDefinitionLevel,
+			column.index,
 			defaultReadBufferSize,
 		)
 	}
diff --git a/limits.go b/limits.go
index bd85e98..9ca4142 100644
--- a/limits.go
+++ b/limits.go
@@ -1,13 +1,16 @@
 package parquet
 
-import "math"
+import (
+	"fmt"
+	"math"
+)
 
 const (
 	// MaxColumnDepth is the maximum column depth supported by this package.
 	MaxColumnDepth = math.MaxInt8
 
 	// MaxColumnIndex is the maximum column index supported by this package.
-	MaxColumnIndex = math.MaxInt8
+	MaxColumnIndex = math.MaxInt16
 
 	// MaxRepetitionLevel is the maximum repetition level supported by this package.
 	MaxRepetitionLevel = math.MaxInt8
@@ -15,3 +18,28 @@ const (
 	// MaxDefinitionLevel is the maximum definition level supported by this package.
 	MaxDefinitionLevel = math.MaxInt8
 )
+
+func makeRepetitionLevel(i int) int8 {
+	checkIndexRange("repetition level", i, 0, MaxRepetitionLevel)
+	return int8(i)
+}
+
+func makeDefinitionLevel(i int) int8 {
+	checkIndexRange("definition level", i, 0, MaxDefinitionLevel)
+	return int8(i)
+}
+
+func makeColumnIndex(i int) int16 {
+	checkIndexRange("column index", i, 0, MaxColumnIndex)
+	return int16(i)
+}
+
+func checkIndexRange(typ string, i, min, max int) {
+	if i < min || i > max {
+		panic(errIndexOutOfRange(typ, i, min, max))
+	}
+}
+
+func errIndexOutOfRange(typ string, i, min, max int) error {
+	return fmt.Errorf("%s out of range: %d not in [%d:%d]", typ, i, min, max)
+}
diff --git a/merge.go b/merge.go
index 7087beb..b9b92c5 100644
--- a/merge.go
+++ b/merge.go
@@ -36,9 +36,9 @@ type mergedRowGroupRowReader struct {
 
 func (r *mergedRowGroupRowReader) init(m *mergedRowGroup) {
 	if r.schema != nil {
-		numColumns := numColumnsOf(r.schema)
+		numColumns := numLeafColumnsOf(r.schema)
 		cursors := make([]bufferedRowGroupCursor, len(m.rowGroups))
-		buffers := make([][]Value, numColumns*len(m.rowGroups))
+		buffers := make([][]Value, int(numColumns)*len(m.rowGroups))
 
 		for i, rowGroup := range m.rowGroups {
 			cursors[i].reader = rowGroup.Rows()
@@ -197,11 +197,11 @@ func (r *mergedRowGroupRowReader) Pop() interface{} {
 type rowGroupCursor interface {
 	readRow(Row) (Row, error)
 	readNext() error
-	nextRowValuesOf([]Value, int) []Value
+	nextRowValuesOf([]Value, int16) []Value
 }
 
 type columnSortFunc struct {
-	columnIndex int
+	columnIndex int16
 	compare     sortFunc
 }
 
@@ -230,7 +230,7 @@ func (cur *bufferedRowGroupCursor) readNext() (err error) {
 	return nil
 }
 
-func (cur *bufferedRowGroupCursor) nextRowValuesOf(values []Value, columnIndex int) []Value {
+func (cur *bufferedRowGroupCursor) nextRowValuesOf(values []Value, columnIndex int16) []Value {
 	return append(values, cur.columns[columnIndex]...)
 }
 
@@ -250,7 +250,7 @@ func (cur optimizedRowGroupCursor) readNext() error {
 	return nil
 }
 
-func (cur optimizedRowGroupCursor) nextRowValuesOf(values []Value, columnIndex int) []Value {
+func (cur optimizedRowGroupCursor) nextRowValuesOf(values []Value, columnIndex int16) []Value {
 	col := &cur.columns[columnIndex]
 	err := col.readValues()
 	if err != nil {
diff --git a/node.go b/node.go
index 2f76edb..27a2ce2 100644
--- a/node.go
+++ b/node.go
@@ -443,22 +443,22 @@ func isMap(node Node) bool {
 	return logicalType != nil && logicalType.Map != nil
 }
 
-func numColumnsOf(node Node) int {
-	return maxColumnIndexOf(node, 0)
+func numLeafColumnsOf(node Node) int16 {
+	return makeColumnIndex(numLeafColumns(node, 0))
 }
 
-func maxColumnIndexOf(node Node, columnIndex int) int {
+func numLeafColumns(node Node, columnIndex int) int {
 	if isLeaf(node) {
 		return columnIndex + 1
 	}
 
 	if indexedNode, ok := unwrap(node).(IndexedNode); ok {
 		for i, n := 0, indexedNode.NumChildren(); i < n; i++ {
-			columnIndex = maxColumnIndexOf(indexedNode.ChildByIndex(i), columnIndex)
+			columnIndex = numLeafColumns(indexedNode.ChildByIndex(i), columnIndex)
 		}
 	} else {
 		for _, name := range node.ChildNames() {
-			columnIndex = maxColumnIndexOf(node.ChildByName(name), columnIndex)
+			columnIndex = numLeafColumns(node.ChildByName(name), columnIndex)
 		}
 	}
 
diff --git a/page.go b/page.go
index 63280d5..fcbca9c 100644
--- a/page.go
+++ b/page.go
@@ -199,17 +199,17 @@ func forEachPageSlice(page BufferedPage, wantSize int64, do func(BufferedPage) e
 
 type errorPage struct {
 	err         error
-	columnIndex int
+	columnIndex int16
 }
 
-func newErrorPage(columnIndex int, msg string, args ...interface{}) *errorPage {
+func newErrorPage(columnIndex int16, msg string, args ...interface{}) *errorPage {
 	return &errorPage{
 		err:         fmt.Errorf(msg, args...),
 		columnIndex: columnIndex,
 	}
 }
 
-func (page *errorPage) Column() int                    { return page.columnIndex }
+func (page *errorPage) Column() int                    { return int(page.columnIndex) }
 func (page *errorPage) Dictionary() Dictionary         { return nil }
 func (page *errorPage) NumRows() int64                 { return 0 }
 func (page *errorPage) NumValues() int64               { return 0 }
@@ -549,7 +549,7 @@ func (r *repeatedPageReader) ReadValues(values []Value) (n int, err error) {
 
 type booleanPage struct {
 	values      []bool
-	columnIndex int8
+	columnIndex int16
 }
 
 func (page *booleanPage) Column() int { return int(^page.columnIndex) }
@@ -671,7 +671,7 @@ func (r *booleanPageReader) ReadValues(values []Value) (n int, err error) {
 
 type int32Page struct {
 	values      []int32
-	columnIndex int8
+	columnIndex int16
 }
 
 func (page *int32Page) Column() int { return int(^page.columnIndex) }
@@ -759,7 +759,7 @@ func (r *int32PageReader) ReadValues(values []Value) (n int, err error) {
 
 type int64Page struct {
 	values      []int64
-	columnIndex int8
+	columnIndex int16
 }
 
 func (page *int64Page) Column() int { return int(^page.columnIndex) }
@@ -847,7 +847,7 @@ func (r *int64PageReader) ReadValues(values []Value) (n int, err error) {
 
 type int96Page struct {
 	values      []deprecated.Int96
-	columnIndex int8
+	columnIndex int16
 }
 
 func (page *int96Page) Column() int { return int(^page.columnIndex) }
@@ -937,7 +937,7 @@ func (r *int96PageReader) ReadValues(values []Value) (n int, err error) {
 
 type floatPage struct {
 	values      []float32
-	columnIndex int8
+	columnIndex int16
 }
 
 func (page *floatPage) Column() int { return int(^page.columnIndex) }
@@ -1025,7 +1025,7 @@ func (r *floatPageReader) ReadValues(values []Value) (n int, err error) {
 
 type doublePage struct {
 	values      []float64
-	columnIndex int8
+	columnIndex int16
 }
 
 func (page *doublePage) Column() int { return int(^page.columnIndex) }
@@ -1113,7 +1113,7 @@ func (r *doublePageReader) ReadValues(values []Value) (n int, err error) {
 
 type byteArrayPage struct {
 	values      encoding.ByteArrayList
-	columnIndex int8
+	columnIndex int16
 }
 
 func (page *byteArrayPage) Column() int { return int(^page.columnIndex) }
@@ -1257,7 +1257,7 @@ func (r *byteArrayPageReader) ReadValues(values []Value) (n int, err error) {
 type fixedLenByteArrayPage struct {
 	size        int
 	data        []byte
-	columnIndex int8
+	columnIndex int16
 }
 
 func (page *fixedLenByteArrayPage) Column() int { return int(^page.columnIndex) }
diff --git a/page_reader.go b/page_reader.go
index 0e2425c..3782508 100644
--- a/page_reader.go
+++ b/page_reader.go
@@ -19,13 +19,13 @@ type dataPageReader struct {
 	numValues          int
 	maxRepetitionLevel int8
 	maxDefinitionLevel int8
-	columnIndex        int8
+	columnIndex        int16
 	repetitions        levelReader
 	definitions        levelReader
 	values             ValueDecoder
 }
 
-func newDataPageReader(typ Type, maxRepetitionLevel, maxDefinitionLevel, columnIndex int8, bufferSize int) *dataPageReader {
+func newDataPageReader(typ Type, maxRepetitionLevel, maxDefinitionLevel int8, columnIndex int16, bufferSize int) *dataPageReader {
 	bufferSize /= 2
 	repetitionBufferSize := 0
 	definitionBufferSize := 0
diff --git a/row.go b/row.go
index d814bf4..e0bcee5 100644
--- a/row.go
+++ b/row.go
@@ -37,8 +37,8 @@ func (row Row) Equal(other Row) bool {
 	return true
 }
 
-func (row Row) startsWith(columnIndex int) bool {
-	return len(row) > 0 && int(row[0].Column()) == columnIndex
+func (row Row) startsWith(columnIndex int16) bool {
+	return len(row) > 0 && row[0].Column() == int(columnIndex)
 }
 
 // RowSeeker is an interface implemented by readers of parquet rows which can be
@@ -311,7 +311,7 @@ type levels struct {
 
 type deconstructFunc func(Row, levels, reflect.Value) Row
 
-func deconstructFuncOf(columnIndex int, node Node) (int, deconstructFunc) {
+func deconstructFuncOf(columnIndex int16, node Node) (int16, deconstructFunc) {
 	switch {
 	case node.Optional():
 		return deconstructFuncOfOptional(columnIndex, node)
@@ -327,7 +327,7 @@ func deconstructFuncOf(columnIndex int, node Node) (int, deconstructFunc) {
 }
 
 //go:noinline
-func deconstructFuncOfOptional(columnIndex int, node Node) (int, deconstructFunc) {
+func deconstructFuncOfOptional(columnIndex int16, node Node) (int16, deconstructFunc) {
 	columnIndex, deconstruct := deconstructFuncOf(columnIndex, Required(node))
 	return columnIndex, func(row Row, levels levels, value reflect.Value) Row {
 		if value.IsValid() {
@@ -345,7 +345,7 @@ func deconstructFuncOfOptional(columnIndex int, node Node) (int, deconstructFunc
 }
 
 //go:noinline
-func deconstructFuncOfRepeated(columnIndex int, node Node) (int, deconstructFunc) {
+func deconstructFuncOfRepeated(columnIndex int16, node Node) (int16, deconstructFunc) {
 	columnIndex, deconstruct := deconstructFuncOf(columnIndex, Required(node))
 	return columnIndex, func(row Row, levels levels, value reflect.Value) Row {
 		if !value.IsValid() || value.Len() == 0 {
@@ -364,7 +364,7 @@ func deconstructFuncOfRepeated(columnIndex int, node Node) (int, deconstructFunc
 	}
 }
 
-func deconstructFuncOfRequired(columnIndex int, node Node) (int, deconstructFunc) {
+func deconstructFuncOfRequired(columnIndex int16, node Node) (int16, deconstructFunc) {
 	switch {
 	case isLeaf(node):
 		return deconstructFuncOfLeaf(columnIndex, node)
@@ -373,12 +373,12 @@ func deconstructFuncOfRequired(columnIndex int, node Node) (int, deconstructFunc
 	}
 }
 
-func deconstructFuncOfList(columnIndex int, node Node) (int, deconstructFunc) {
+func deconstructFuncOfList(columnIndex int16, node Node) (int16, deconstructFunc) {
 	return deconstructFuncOf(columnIndex, Repeated(listElementOf(node)))
 }
 
 //go:noinline
-func deconstructFuncOfMap(columnIndex int, node Node) (int, deconstructFunc) {
+func deconstructFuncOfMap(columnIndex int16, node Node) (int16, deconstructFunc) {
 	keyValue := mapKeyValueOf(node)
 	keyValueType := keyValue.GoType()
 	keyValueElem := keyValueType.Elem()
@@ -409,7 +409,7 @@ func deconstructFuncOfMap(columnIndex int, node Node) (int, deconstructFunc) {
 }
 
 //go:noinline
-func deconstructFuncOfGroup(columnIndex int, node Node) (int, deconstructFunc) {
+func deconstructFuncOfGroup(columnIndex int16, node Node) (int16, deconstructFunc) {
 	names := node.ChildNames()
 	funcs := make([]deconstructFunc, len(names))
 
@@ -444,12 +444,12 @@ func deconstructFuncOfGroup(columnIndex int, node Node) (int, deconstructFunc) {
 }
 
 //go:noinline
-func deconstructFuncOfLeaf(columnIndex int, node Node) (int, deconstructFunc) {
+func deconstructFuncOfLeaf(columnIndex int16, node Node) (int16, deconstructFunc) {
 	if columnIndex > MaxColumnIndex {
 		panic("row cannot be deconstructed because it has more than 127 columns")
 	}
 	kind := node.Type().Kind()
-	valueColumnIndex := ^int8(columnIndex)
+	valueColumnIndex := ^columnIndex
 	return columnIndex + 1, func(row Row, levels levels, value reflect.Value) Row {
 		v := Value{}
 
@@ -466,7 +466,7 @@ func deconstructFuncOfLeaf(columnIndex int, node Node) (int, deconstructFunc) {
 
 type reconstructFunc func(reflect.Value, levels, Row) (Row, error)
 
-func reconstructFuncOf(columnIndex int, node Node) (int, reconstructFunc) {
+func reconstructFuncOf(columnIndex int16, node Node) (int16, reconstructFunc) {
 	switch {
 	case node.Optional():
 		return reconstructFuncOfOptional(columnIndex, node)
@@ -482,14 +482,14 @@ func reconstructFuncOf(columnIndex int, node Node) (int, reconstructFunc) {
 }
 
 //go:noinline
-func reconstructFuncOfOptional(columnIndex int, node Node) (int, reconstructFunc) {
+func reconstructFuncOfOptional(columnIndex int16, node Node) (int16, reconstructFunc) {
 	nextColumnIndex, reconstruct := reconstructFuncOf(columnIndex, Required(node))
 	rowLength := nextColumnIndex - columnIndex
 	return nextColumnIndex, func(value reflect.Value, levels levels, row Row) (Row, error) {
 		if !row.startsWith(columnIndex) {
 			return row, fmt.Errorf("row is missing optional column %d", columnIndex)
 		}
-		if len(row) < rowLength {
+		if len(row) < int(rowLength) {
 			return row, fmt.Errorf("expected optional column %d to have at least %d values but got %d", columnIndex, rowLength, len(row))
 		}
 
@@ -512,7 +512,7 @@ func reconstructFuncOfOptional(columnIndex int, node Node) (int, reconstructFunc
 }
 
 //go:noinline
-func reconstructFuncOfRepeated(columnIndex int, node Node) (int, reconstructFunc) {
+func reconstructFuncOfRepeated(columnIndex int16, node Node) (int16, reconstructFunc) {
 	nextColumnIndex, reconstruct := reconstructFuncOf(columnIndex, Required(node))
 	rowLength := nextColumnIndex - columnIndex
 	return nextColumnIndex, func(value reflect.Value, lvls levels, row Row) (Row, error) {
@@ -544,11 +544,11 @@ func reconstructFuncOfRepeated(columnIndex int, node Node) (int, reconstructFunc
 	}
 }
 
-func reconstructRepeated(columnIndex, rowLength int, levels levels, row Row, do func(levels, Row) (Row, error)) (Row, error) {
+func reconstructRepeated(columnIndex, rowLength int16, levels levels, row Row, do func(levels, Row) (Row, error)) (Row, error) {
 	if !row.startsWith(columnIndex) {
 		return row, fmt.Errorf("row is missing repeated column %d", columnIndex)
 	}
-	if len(row) < rowLength {
+	if len(row) < int(rowLength) {
 		return row, fmt.Errorf("expected repeated column %d to have at least %d values but got %d", columnIndex, rowLength, len(row))
 	}
 
@@ -569,7 +569,7 @@ func reconstructRepeated(columnIndex, rowLength int, levels levels, row Row, do
 	return row, err
 }
 
-func reconstructFuncOfRequired(columnIndex int, node Node) (int, reconstructFunc) {
+func reconstructFuncOfRequired(columnIndex int16, node Node) (int16, reconstructFunc) {
 	switch {
 	case isLeaf(node):
 		return reconstructFuncOfLeaf(columnIndex, node)
@@ -578,12 +578,12 @@ func reconstructFuncOfRequired(columnIndex int, node Node) (int, reconstructFunc
 	}
 }
 
-func reconstructFuncOfList(columnIndex int, node Node) (int, reconstructFunc) {
+func reconstructFuncOfList(columnIndex int16, node Node) (int16, reconstructFunc) {
 	return reconstructFuncOf(columnIndex, Repeated(listElementOf(node)))
 }
 
 //go:noinline
-func reconstructFuncOfMap(columnIndex int, node Node) (int, reconstructFunc) {
+func reconstructFuncOfMap(columnIndex int16, node Node) (int16, reconstructFunc) {
 	keyValue := mapKeyValueOf(node)
 	keyValueType := keyValue.GoType()
 	keyValueElem := keyValueType.Elem()
@@ -612,10 +612,10 @@ func reconstructFuncOfMap(columnIndex int, node Node) (int, reconstructFunc) {
 }
 
 //go:noinline
-func reconstructFuncOfGroup(columnIndex int, node Node) (int, reconstructFunc) {
+func reconstructFuncOfGroup(columnIndex int16, node Node) (int16, reconstructFunc) {
 	names := node.ChildNames()
 	funcs := make([]reconstructFunc, len(names))
-	columnIndexes := make([]int, len(names))
+	columnIndexes := make([]int16, len(names))
 
 	for i, name := range names {
 		columnIndex, funcs[i] = reconstructFuncOf(columnIndex, node.ChildByName(name))
@@ -647,7 +647,7 @@ func reconstructFuncOfGroup(columnIndex int, node Node) (int, reconstructFunc) {
 }
 
 //go:noinline
-func reconstructFuncOfLeaf(columnIndex int, node Node) (int, reconstructFunc) {
+func reconstructFuncOfLeaf(columnIndex int16, node Node) (int16, reconstructFunc) {
 	return columnIndex + 1, func(value reflect.Value, _ levels, row Row) (Row, error) {
 		if !row.startsWith(columnIndex) {
 			return row, fmt.Errorf("no values found in parquet row for column %d", columnIndex)
diff --git a/row_group.go b/row_group.go
index 5424383..9c517b1 100644
--- a/row_group.go
+++ b/row_group.go
@@ -391,7 +391,7 @@ type emptyRowGroup struct {
 func newEmptyRowGroup(schema *Schema) *emptyRowGroup {
 	g := &emptyRowGroup{
 		schema:  schema,
-		columns: make([]emptyColumnChunk, numColumnsOf(schema)),
+		columns: make([]emptyColumnChunk, numLeafColumnsOf(schema)),
 	}
 	forEachLeafColumnOf(schema, func(leaf leafColumn) {
 		g.columns[leaf.columnIndex].typ = leaf.node.Type()
@@ -409,11 +409,11 @@ func (g *emptyRowGroup) Rows() Rows                      { return emptyRowReader
 
 type emptyColumnChunk struct {
 	typ    Type
-	column int
+	column int16
 }
 
 func (c *emptyColumnChunk) Type() Type               { return c.typ }
-func (c *emptyColumnChunk) Column() int              { return c.column }
+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 }
diff --git a/schema.go b/schema.go
index 5a3c591..89e64ad 100644
--- a/schema.go
+++ b/schema.go
@@ -88,7 +88,11 @@ func schemaOf(model reflect.Type) *Schema {
 }
 
 // NewSchema constructs a new Schema object with the given name and root node.
+//
+// The function panics if Node contains more leaf columns than supported by the
+// package (see parquet.MaxColumnIndex).
 func NewSchema(name string, root Node) *Schema {
+	_ = numLeafColumnsOf(root)
 	return &Schema{
 		name:        name,
 		root:        root,
diff --git a/type.go b/type.go
index 6ff4d80..8fb60f2 100644
--- a/type.go
+++ b/type.go
@@ -14,7 +14,7 @@ import (
 
 // Kind is an enumeration type representing the physical types supported by the
 // parquet type system.
-type Kind int16
+type Kind int8
 
 const (
 	Boolean           Kind = Kind(format.Boolean)
@@ -209,7 +209,7 @@ func (t booleanType) NewDictionary(bufferSize int) Dictionary {
 }
 
 func (t booleanType) NewColumnBuffer(columnIndex, bufferSize int) ColumnBuffer {
-	return newBooleanColumnBuffer(t, columnIndex, bufferSize)
+	return newBooleanColumnBuffer(t, makeColumnIndex(columnIndex), bufferSize)
 }
 
 func (t booleanType) NewValueDecoder(bufferSize int) ValueDecoder {
@@ -241,7 +241,7 @@ func (t int32Type) NewDictionary(bufferSize int) Dictionary {
 }
 
 func (t int32Type) NewColumnBuffer(columnIndex, bufferSize int) ColumnBuffer {
-	return newInt32ColumnBuffer(t, columnIndex, bufferSize)
+	return newInt32ColumnBuffer(t, makeColumnIndex(columnIndex), bufferSize)
 }
 
 func (t int32Type) NewValueDecoder(bufferSize int) ValueDecoder {
@@ -273,7 +273,7 @@ func (t int64Type) NewDictionary(bufferSize int) Dictionary {
 }
 
 func (t int64Type) NewColumnBuffer(columnIndex, bufferSize int) ColumnBuffer {
-	return newInt64ColumnBuffer(t, columnIndex, bufferSize)
+	return newInt64ColumnBuffer(t, makeColumnIndex(columnIndex), bufferSize)
 }
 
 func (t int64Type) NewValueDecoder(bufferSize int) ValueDecoder {
@@ -305,7 +305,7 @@ func (t int96Type) NewDictionary(bufferSize int) Dictionary {
 }
 
 func (t int96Type) NewColumnBuffer(columnIndex, bufferSize int) ColumnBuffer {
-	return newInt96ColumnBuffer(t, columnIndex, bufferSize)
+	return newInt96ColumnBuffer(t, makeColumnIndex(columnIndex), bufferSize)
 }
 
 func (t int96Type) NewValueDecoder(bufferSize int) ValueDecoder {
@@ -337,7 +337,7 @@ func (t floatType) NewDictionary(bufferSize int) Dictionary {
 }
 
 func (t floatType) NewColumnBuffer(columnIndex, bufferSize int) ColumnBuffer {
-	return newFloatColumnBuffer(t, columnIndex, bufferSize)
+	return newFloatColumnBuffer(t, makeColumnIndex(columnIndex), bufferSize)
 }
 
 func (t floatType) NewValueDecoder(bufferSize int) ValueDecoder {
@@ -367,7 +367,7 @@ func (t doubleType) NewDictionary(bufferSize int) Dictionary {
 }
 
 func (t doubleType) NewColumnBuffer(columnIndex, bufferSize int) ColumnBuffer {
-	return newDoubleColumnBuffer(t, columnIndex, bufferSize)
+	return newDoubleColumnBuffer(t, makeColumnIndex(columnIndex), bufferSize)
 }
 
 func (t doubleType) NewValueDecoder(bufferSize int) ValueDecoder {
@@ -399,7 +399,7 @@ func (t byteArrayType) NewDictionary(bufferSize int) Dictionary {
 }
 
 func (t byteArrayType) NewColumnBuffer(columnIndex, bufferSize int) ColumnBuffer {
-	return newByteArrayColumnBuffer(t, columnIndex, bufferSize)
+	return newByteArrayColumnBuffer(t, makeColumnIndex(columnIndex), bufferSize)
 }
 
 func (t byteArrayType) NewValueDecoder(bufferSize int) ValueDecoder {
@@ -436,7 +436,7 @@ func (t *fixedLenByteArrayType) NewDictionary(bufferSize int) Dictionary {
 }
 
 func (t *fixedLenByteArrayType) NewColumnBuffer(columnIndex, bufferSize int) ColumnBuffer {
-	return newFixedLenByteArrayColumnBuffer(t, columnIndex, bufferSize)
+	return newFixedLenByteArrayColumnBuffer(t, makeColumnIndex(columnIndex), bufferSize)
 }
 
 func (t *fixedLenByteArrayType) NewValueDecoder(bufferSize int) ValueDecoder {
@@ -591,15 +591,15 @@ func (t *intType) NewDictionary(bufferSize int) Dictionary {
 func (t *intType) NewColumnBuffer(columnIndex, bufferSize int) ColumnBuffer {
 	if t.IsSigned {
 		if t.BitWidth == 64 {
-			return newInt64ColumnBuffer(t, columnIndex, bufferSize)
+			return newInt64ColumnBuffer(t, makeColumnIndex(columnIndex), bufferSize)
 		} else {
-			return newInt32ColumnBuffer(t, columnIndex, bufferSize)
+			return newInt32ColumnBuffer(t, makeColumnIndex(columnIndex), bufferSize)
 		}
 	} else {
 		if t.BitWidth == 64 {
-			return newUint64ColumnBuffer(t, columnIndex, bufferSize)
+			return newUint64ColumnBuffer(t, makeColumnIndex(columnIndex), bufferSize)
 		} else {
-			return newUint32ColumnBuffer(t, columnIndex, bufferSize)
+			return newUint32ColumnBuffer(t, makeColumnIndex(columnIndex), bufferSize)
 		}
 	}
 }
@@ -688,7 +688,7 @@ func (t *stringType) NewDictionary(bufferSize int) Dictionary {
 }
 
 func (t *stringType) NewColumnBuffer(columnIndex, bufferSize int) ColumnBuffer {
-	return newByteArrayColumnBuffer(t, columnIndex, bufferSize)
+	return newByteArrayColumnBuffer(t, makeColumnIndex(columnIndex), bufferSize)
 }
 
 func (t *stringType) NewValueDecoder(bufferSize int) ValueDecoder {
@@ -739,7 +739,7 @@ func (t *uuidType) NewDictionary(bufferSize int) Dictionary {
 }
 
 func (t *uuidType) NewColumnBuffer(columnIndex, bufferSize int) ColumnBuffer {
-	return newFixedLenByteArrayColumnBuffer(t, columnIndex, bufferSize)
+	return newFixedLenByteArrayColumnBuffer(t, makeColumnIndex(columnIndex), bufferSize)
 }
 
 func (t *uuidType) NewValueDecoder(bufferSize int) ValueDecoder {
@@ -792,7 +792,7 @@ func (t *enumType) NewDictionary(bufferSize int) Dictionary {
 }
 
 func (t *enumType) NewColumnBuffer(columnIndex, bufferSize int) ColumnBuffer {
-	return newByteArrayColumnBuffer(t, columnIndex, bufferSize)
+	return newByteArrayColumnBuffer(t, makeColumnIndex(columnIndex), bufferSize)
 }
 
 func (t *enumType) NewValueDecoder(bufferSize int) ValueDecoder {
@@ -845,7 +845,7 @@ func (t *jsonType) NewDictionary(bufferSize int) Dictionary {
 }
 
 func (t *jsonType) NewColumnBuffer(columnIndex, bufferSize int) ColumnBuffer {
-	return newByteArrayColumnBuffer(t, columnIndex, bufferSize)
+	return newByteArrayColumnBuffer(t, makeColumnIndex(columnIndex), bufferSize)
 }
 
 func (t *jsonType) NewValueDecoder(bufferSize int) ValueDecoder {
@@ -894,7 +894,7 @@ func (t *bsonType) NewDictionary(bufferSize int) Dictionary {
 }
 
 func (t *bsonType) NewColumnBuffer(columnIndex, bufferSize int) ColumnBuffer {
-	return newByteArrayColumnBuffer(t, columnIndex, bufferSize)
+	return newByteArrayColumnBuffer(t, makeColumnIndex(columnIndex), bufferSize)
 }
 
 func (t *bsonType) NewValueDecoder(bufferSize int) ValueDecoder {
@@ -939,7 +939,7 @@ func (t *dateType) NewDictionary(bufferSize int) Dictionary {
 }
 
 func (t *dateType) NewColumnBuffer(columnIndex, bufferSize int) ColumnBuffer {
-	return newInt32ColumnBuffer(t, columnIndex, bufferSize)
+	return newInt32ColumnBuffer(t, makeColumnIndex(columnIndex), bufferSize)
 }
 
 func (t *dateType) NewValueDecoder(bufferSize int) ValueDecoder {
@@ -1064,9 +1064,9 @@ func (t *timeType) NewDictionary(bufferSize int) Dictionary {
 
 func (t *timeType) NewColumnBuffer(columnIndex, bufferSize int) ColumnBuffer {
 	if t.Unit.Millis != nil {
-		return newInt32ColumnBuffer(t, columnIndex, bufferSize)
+		return newInt32ColumnBuffer(t, makeColumnIndex(columnIndex), bufferSize)
 	} else {
-		return newInt64ColumnBuffer(t, columnIndex, bufferSize)
+		return newInt64ColumnBuffer(t, makeColumnIndex(columnIndex), bufferSize)
 	}
 }
 
@@ -1123,7 +1123,7 @@ func (t *timestampType) NewDictionary(bufferSize int) Dictionary {
 }
 
 func (t *timestampType) NewColumnBuffer(columnIndex, bufferSize int) ColumnBuffer {
-	return newInt64ColumnBuffer(t, columnIndex, bufferSize)
+	return newInt64ColumnBuffer(t, makeColumnIndex(columnIndex), bufferSize)
 }
 
 func (t *timestampType) NewValueDecoder(bufferSize int) ValueDecoder {
diff --git a/value.go b/value.go
index 2a1323d..ae02e83 100644
--- a/value.go
+++ b/value.go
@@ -31,13 +31,12 @@ type Value struct {
 	// data
 	ptr *byte
 	u64 uint64
-	u32 uint32
 	// type
 	kind int8 // XOR(Kind) so the zero-value is <null>
 	// levels
 	definitionLevel int8
 	repetitionLevel int8
-	columnIndex     int8 // XOR so the zero-value is -1
+	columnIndex     int16 // XOR so the zero-value is -1
 }
 
 // ValueReader is an interface implemented by types that support reading
@@ -428,7 +427,7 @@ func makeValue(k Kind, v reflect.Value) Value {
 func makeValueBoolean(value bool) Value {
 	v := Value{kind: ^int8(Boolean)}
 	if value {
-		v.u32 = 1
+		v.u64 = 1
 	}
 	return v
 }
@@ -436,7 +435,7 @@ func makeValueBoolean(value bool) Value {
 func makeValueInt32(value int32) Value {
 	return Value{
 		kind: ^int8(Int32),
-		u32:  uint32(value),
+		u64:  uint64(value),
 	}
 }
 
@@ -448,17 +447,25 @@ func makeValueInt64(value int64) Value {
 }
 
 func makeValueInt96(value deprecated.Int96) Value {
+	// TODO: this is highly inefficient because we need a heap allocation to
+	// store the value; we don't expect INT96 to be used frequently since it
+	// is a deprecated feature of parquet, and it helps keep the Value type
+	// compact for all the other more common cases.
+	bits := [12]byte{}
+	binary.LittleEndian.PutUint32(bits[0:4], value[0])
+	binary.LittleEndian.PutUint32(bits[4:8], value[1])
+	binary.LittleEndian.PutUint32(bits[8:12], value[2])
 	return Value{
 		kind: ^int8(Int96),
-		u64:  uint64(value[0]) | (uint64(value[1]) << 32),
-		u32:  value[2],
+		ptr:  &bits[0],
+		u64:  12, // set the length so we can use the ByteArray method
 	}
 }
 
 func makeValueFloat(value float32) Value {
 	return Value{
 		kind: ^int8(Float),
-		u32:  math.Float32bits(value),
+		u64:  uint64(math.Float32bits(value)),
 	}
 }
 
@@ -509,19 +516,19 @@ func (v Value) Kind() Kind { return ^Kind(v.kind) }
 func (v Value) IsNull() bool { return v.kind == 0 }
 
 // Boolean returns v as a bool, assuming the underlying type is BOOLEAN.
-func (v Value) Boolean() bool { return v.u32 != 0 }
+func (v Value) Boolean() bool { return v.u64 != 0 }
 
 // Int32 returns v as a int32, assuming the underlying type is INT32.
-func (v Value) Int32() int32 { return int32(v.u32) }
+func (v Value) Int32() int32 { return int32(v.u64) }
 
 // Int64 returns v as a int64, assuming the underlying type is INT64.
 func (v Value) Int64() int64 { return int64(v.u64) }
 
 // Int96 returns v as a int96, assuming the underlying type is INT96.
-func (v Value) Int96() deprecated.Int96 { return makeInt96(v.u64, v.u32) }
+func (v Value) Int96() deprecated.Int96 { return makeInt96(v.ByteArray()) }
 
 // Float returns v as a float32, assuming the underlying type is FLOAT.
-func (v Value) Float() float32 { return math.Float32frombits(v.u32) }
+func (v Value) Float() float32 { return math.Float32frombits(uint32(v.u64)) }
 
 // Double returns v as a float64, assuming the underlying type is DOUBLE.
 func (v Value) Double() float64 { return math.Float64frombits(v.u64) }
@@ -534,15 +541,15 @@ func (v Value) Double() float64 { return math.Float64frombits(v.u64) }
 func (v Value) ByteArray() []byte { return unsafe.Slice(v.ptr, int(v.u64)) }
 
 // RepetitionLevel returns the repetition level of v.
-func (v Value) RepetitionLevel() int8 { return v.repetitionLevel }
+func (v Value) RepetitionLevel() int { return int(v.repetitionLevel) }
 
 // DefinitionLevel returns the definition level of v.
-func (v Value) DefinitionLevel() int8 { return v.definitionLevel }
+func (v Value) DefinitionLevel() int { return int(v.definitionLevel) }
 
 // Column returns the column index within the row that v was created from.
 //
 // Returns -1 if the value does not carry a column index.
-func (v Value) Column() int8 { return ^v.columnIndex }
+func (v Value) Column() int { return int(^v.columnIndex) }
 
 // Bytes returns the binary representation of v.
 //
@@ -553,22 +560,18 @@ func (v Value) Bytes() []byte { return v.AppendBytes(nil) }
 //
 // If v is the null value, b is returned unchanged.
 func (v Value) AppendBytes(b []byte) []byte {
-	buf := [12]byte{}
+	buf := [8]byte{}
 	switch v.Kind() {
 	case Boolean:
-		binary.LittleEndian.PutUint32(buf[:4], v.u32)
+		binary.LittleEndian.PutUint32(buf[:4], uint32(v.u64))
 		return append(b, buf[0])
 	case Int32, Float:
-		binary.LittleEndian.PutUint32(buf[:4], v.u32)
+		binary.LittleEndian.PutUint32(buf[:4], uint32(v.u64))
 		return append(b, buf[:4]...)
 	case Int64, Double:
 		binary.LittleEndian.PutUint64(buf[:8], v.u64)
 		return append(b, buf[:8]...)
-	case Int96:
-		binary.LittleEndian.PutUint64(buf[:8], v.u64)
-		binary.LittleEndian.PutUint32(buf[8:], v.u32)
-		return append(b, buf[:12]...)
-	case ByteArray, FixedLenByteArray:
+	case ByteArray, FixedLenByteArray, Int96:
 		return append(b, v.ByteArray()...)
 	default:
 		return b
@@ -697,16 +700,10 @@ func (v Value) GoString() string {
 // set to the values passed as arguments.
 //
 // The method panics if either argument is negative.
-func (v Value) Level(repetitionLevel, definitionLevel, columnIndex int8) Value {
-	if repetitionLevel < 0 {
-		panic("cannot create a value with a negative repetition level")
-	}
-	if definitionLevel < 0 {
-		panic("cannot create a value with a negative definition level")
-	}
-	v.repetitionLevel = repetitionLevel
-	v.definitionLevel = definitionLevel
-	v.columnIndex = ^columnIndex
+func (v Value) Level(repetitionLevel, definitionLevel, columnIndex int) Value {
+	v.repetitionLevel = makeRepetitionLevel(repetitionLevel)
+	v.definitionLevel = makeDefinitionLevel(definitionLevel)
+	v.columnIndex = ^makeColumnIndex(columnIndex)
 	return v
 }
 
@@ -720,11 +717,11 @@ func (v Value) Clone() Value {
 	return v
 }
 
-func makeInt96(lo uint64, hi uint32) (i96 deprecated.Int96) {
+func makeInt96(bits []byte) (i96 deprecated.Int96) {
 	return deprecated.Int96{
-		0: uint32(lo),
-		1: uint32(lo >> 32),
-		2: hi,
+		2: binary.LittleEndian.Uint32(bits[8:12]),
+		1: binary.LittleEndian.Uint32(bits[4:8]),
+		0: binary.LittleEndian.Uint32(bits[0:4]),
 	}
 }
 
@@ -861,9 +858,7 @@ func parseValue(kind Kind, data []byte) (val Value, err error) {
 		}
 	case Int96:
 		if len(data) == 12 {
-			lo := binary.LittleEndian.Uint64(data[0:8])
-			hi := binary.LittleEndian.Uint32(data[8:12])
-			val = makeValueInt96(makeInt96(lo, hi))
+			val = makeValueInt96(makeInt96(data))
 		}
 	case Float:
 		if len(data) == 4 {


Test patch:
diff --git a/buffer_test.go b/buffer_test.go
index f1f0077..d770f11 100644
--- a/buffer_test.go
+++ b/buffer_test.go
@@ -238,8 +238,8 @@ func descending(typ parquet.Type, values []parquet.Value) {
 }
 
 func testBuffer(t *testing.T, node parquet.Node, reader parquet.ValueReader, buffer *parquet.Buffer, encoder encoding.Encoder, values []interface{}, sortFunc sortFunc) {
-	repetitionLevel := int8(0)
-	definitionLevel := int8(0)
+	repetitionLevel := 0
+	definitionLevel := 0
 	if !node.Required() {
 		definitionLevel = 1
 	}