Repository: cockroachdb/pebble
Pull request #3762 (2024-07-16 21:16:39)

colblk: define block structure
This commit defines the structure of a columnar block, defining a block header and footer, and routines for reading columns from an encoded block. Additionally, the interfaces around ColumnEncodings are refactored so that individual data types are responsible for encoding metadata about their encoding within the column data itself if necessary. Currently, this means the delta encoding for uint columns is moved into the first byte of the uint column data.

cockroach-teamcity: 

<!-- Reviewable:start -->
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/cockroachdb/pebble/3762)
<!-- Reviewable:end -->


Patch:
diff --git a/sstable/colblk/bitmap.go b/sstable/colblk/bitmap.go
index 39a3ff85..2fd1429d 100644
--- a/sstable/colblk/bitmap.go
+++ b/sstable/colblk/bitmap.go
@@ -34,13 +34,14 @@ type Bitmap struct {
 // bits. No bounds checking is performed, so the caller must guarantee the
 // bitmap is appropriately sized and the provided bitCount correctly identifies
 // the number of bits in the bitmap.
-func MakeBitmap(b []byte, bitCount int) Bitmap {
-	if len(b) != bitmapRequiredSize(bitCount) {
-		panic(errors.AssertionFailedf("bitmap of %d bits requires at %d bytes; provided with %d-byte slice",
-			bitCount, bitmapRequiredSize(bitCount), len(b)))
+func MakeBitmap(b []byte, off uint32, bitCount int) Bitmap {
+	if len(b) < int(off)+bitmapRequiredSize(bitCount) {
+		panic(errors.AssertionFailedf("bitmap of %d bits requires at least %d bytes; provided with %d-byte slice",
+			bitCount, bitmapRequiredSize(bitCount), len(b[off:])))
 	}
+	off = align(off, align64)
 	return Bitmap{
-		data:     makeUnsafeRawSlice[uint64](unsafe.Pointer(&b[0])),
+		data:     makeUnsafeRawSlice[uint64](unsafe.Pointer(&b[off])),
 		bitCount: bitCount,
 	}
 }
@@ -201,6 +202,9 @@ type BitmapBuilder struct {
 	words []uint64
 }
 
+// Assert that BitmapBuilder implements ColumnWriter.
+var _ ColumnWriter = (*BitmapBuilder)(nil)
+
 func bitmapRequiredSize(total int) int {
 	nWords := (total + 63) >> 6          // divide by 64
 	nSummaryWords := (nWords + 63) >> 6  // divide by 64
@@ -231,6 +235,9 @@ func (b *BitmapBuilder) Reset() {
 // NumColumns implements the ColumnWriter interface.
 func (b *BitmapBuilder) NumColumns() int { return 1 }
 
+// DataType implements the ColumnWriter interface.
+func (b *BitmapBuilder) DataType(int) DataType { return DataTypeBool }
+
 // Size implements the ColumnWriter interface.
 func (b *BitmapBuilder) Size(rows int, offset uint32) uint32 {
 	offset = align(offset, align64)
@@ -251,7 +258,7 @@ func (b *BitmapBuilder) Invert(nRows int) {
 
 // Finish finalizes the bitmap, computing the per-word summary bitmap and
 // writing the resulting data to buf at offset.
-func (b *BitmapBuilder) Finish(col, nRows int, offset uint32, buf []byte) (uint32, ColumnDesc) {
+func (b *BitmapBuilder) Finish(col, nRows int, offset uint32, buf []byte) uint32 {
 	offset = alignWithZeroes(buf, offset, align64)
 	dest := makeUnsafeRawSlice[uint64](unsafe.Pointer(&buf[offset]))
 
@@ -294,8 +301,7 @@ func (b *BitmapBuilder) Finish(col, nRows int, offset uint32, buf []byte) (uint3
 		}
 		dest.set(nBitmapWords+i, summaryWord)
 	}
-	offset += uint32(nSummaryWords) << align64Shift
-	return offset, ColumnDesc{DataType: DataTypeBool, Encoding: EncodingDefault}
+	return offset + uint32(nSummaryWords)<<align64Shift
 }
 
 // WriteDebug implements the ColumnWriter interface.
@@ -304,7 +310,10 @@ func (b *BitmapBuilder) WriteDebug(w io.Writer, rows int) {
 	fmt.Fprint(w, "bitmap")
 }
 
-func bitmapToBinFormatter(f *binfmt.Formatter, rows int) int {
+func bitmapToBinFormatter(f *binfmt.Formatter, rows int) {
+	if aligned := align(f.Offset(), 8); aligned-f.Offset() != 0 {
+		f.HexBytesln(aligned-f.Offset(), "padding to align to 64-bit boundary")
+	}
 	bitmapWords := (rows + 63) / 64
 	for i := 0; i < bitmapWords; i++ {
 		f.Line(8).Append("b ").Binary(8).Done("bitmap word %d", i)
@@ -313,5 +322,4 @@ func bitmapToBinFormatter(f *binfmt.Formatter, rows int) int {
 	for i := 0; i < summaryWords; i++ {
 		f.Line(8).Append("b ").Binary(8).Done("bitmap summary word %d-%d", i*64, i*64+63)
 	}
-	return (bitmapWords + summaryWords) * align64
 }
diff --git a/sstable/colblk/block.go b/sstable/colblk/block.go
index 7f30bd36..929c2054 100644
--- a/sstable/colblk/block.go
+++ b/sstable/colblk/block.go
@@ -4,8 +4,55 @@
 
 // Package colblk implements a columnar block format.
 //
-// TODO(jackson): Describe the format once we start merging the higher-level
-// block format.
+// The columnar block format organizes data into columns. The number of columns
+// and their data types are dependent on the use and are configurable.
+//
+// # Format
+//
+// Every columnar block begins with a header describing the structure and schema
+// of the block. Then columns values are encoded in sequence. The block ends
+// with a single padding byte.
+//
+// The block header begins with:
+// - Version number (1 byte)
+// - The number of columns in the block (2 bytes)
+// - The number of rows in the block (4 bytes)
+//
+// Then follows a column-header for each column. Each column header encodes the
+// data type (1 byte) and the offset into the block where the column data begins
+// (4 bytes).
+//
+//	+-----------+
+//	| Vers (1B) |
+//	+-------------------+--------------------------------+
+//	| # columns (2B)    | # of rows (4B)                 |
+//	+-----------+-------+---------------------+----------+
+//	| Type (1B) | Page offset (4B)                | Col 0
+//	+-----------+---------------------------------+
+//	| Type (1B) | Page offset (4B)                | Col 1
+//	+-----------+---------------------------------+
+//	| ...	    | ...                             |
+//	+-----------+---------------------------------+
+//	| Type (1B) | Page offset (4B)                | Col n-1
+//	+-----------+----------------------------------
+//	|  column 0 data                                ...
+//	+----------------------------------------------
+//	|  column 1 data                                ...
+//	+----------------------------------------------
+//	| ...
+//	+----------------------------------------------
+//	|  column n-1 data                              ...
+//	+-------------+--------------------------------
+//	| Unused (1B) |
+//	+-------------+
+//
+// The encoding of column data itself is dependent on the data type.
+//
+// The trailing padding byte exists to allow columns to represent the end of
+// column data using a pointer to the byte after the end of the column. The
+// padding byte ensures that the pointer will always fall within the allocated
+// memory. Without the padding byte, such a pointer would be invalid according
+// to Go's pointer rules.
 //
 // # Alignment
 //
@@ -14,3 +61,262 @@
 // word-alignment, the writer can align the offset into the block buffer
 // appropriately to ensure that the data is word-aligned.
 package colblk
+
+import (
+	"cmp"
+	"encoding/binary"
+	"fmt"
+	"unsafe"
+
+	"github.com/cockroachdb/errors"
+	"github.com/cockroachdb/pebble/internal/aligned"
+	"github.com/cockroachdb/pebble/internal/binfmt"
+	"github.com/cockroachdb/pebble/internal/invariants"
+)
+
+// Version indicates the version of the columnar block format encoded. The
+// version byte is always the first byte within the block. This ensures that
+// readers can switch on the version before reading the rest of the block.
+type Version uint8
+
+const (
+	// Version1 is the first version of the columnar block format.
+	Version1 Version = 0x01
+)
+
+const blockHeaderBaseSize = 7
+const columnHeaderSize = 5
+
+// Header holds the metadata extracted from a columnar block's header.
+type Header struct {
+	Version Version
+	// Columns holds the number of columns encoded within the block.
+	Columns uint16
+	// Rows holds the number of rows encoded within the block.
+	Rows uint32
+}
+
+// String implements the fmt.Stringer interface, returning a human-readable
+// representation of the block header.
+func (h Header) String() string {
+	return fmt.Sprintf("Version=%v; Columns=%d; Rows=%d", h.Version, h.Columns, h.Rows)
+}
+
+// Encode encodes the header to the provided buf. The length of buf must be at
+// least 7 bytes.
+func (h Header) Encode(buf []byte) {
+	buf[0] = byte(h.Version)
+	binary.LittleEndian.PutUint16(buf[1:], h.Columns)
+	binary.LittleEndian.PutUint32(buf[1+align16:], h.Rows)
+}
+
+// blockHeaderSize returns the size of the block header, including column
+// headers, for a block with the specified number of columns and optionally a
+// custom header size.
+func blockHeaderSize(cols int, customHeaderSize int) uint32 {
+	// Each column has a 1-byte DataType and a 4-byte offset into the block.
+	return uint32(blockHeaderBaseSize + cols*columnHeaderSize + customHeaderSize)
+}
+
+// DecodeHeader reads the block header from the provided serialized columnar
+// block.
+func DecodeHeader(data []byte) Header {
+	return Header{
+		Version: Version(data[0]),
+		Columns: uint16(binary.LittleEndian.Uint16(data[1:])),
+		Rows:    uint32(binary.LittleEndian.Uint32(data[1+align16:])),
+	}
+}
+
+// FinishBlock writes the columnar block to a heap-allocated byte slice.
+// FinishBlock assumes all columns have the same number of rows. If that's not
+// the case, the caller should manually construct their own block.
+func FinishBlock(rows int, writers []ColumnWriter) []byte {
+	size := blockHeaderSize(len(writers), 0)
+	nCols := 0
+	for _, cw := range writers {
+		size = cw.Size(rows, size)
+		nCols += cw.NumColumns()
+	}
+	size++ // +1 for the trailing version byte.
+
+	buf := aligned.ByteSlice(int(size))
+	Header{
+		Version: Version1,
+		Columns: uint16(nCols),
+		Rows:    uint32(rows),
+	}.Encode(buf)
+	pageOffset := blockHeaderSize(len(writers), 0)
+	col := 0
+	for _, cw := range writers {
+		for i := 0; i < cw.NumColumns(); i++ {
+			hi := blockHeaderSize(col, 0)
+			buf[hi] = byte(cw.DataType(i))
+			binary.LittleEndian.PutUint32(buf[hi+1:], pageOffset)
+			pageOffset = cw.Finish(i, rows, pageOffset, buf)
+			col++
+		}
+	}
+	buf[pageOffset] = 0x00 // Padding byte
+	pageOffset++
+	if invariants.Enabled && pageOffset != size {
+		panic(fmt.Sprintf("expected pageOffset=%d to be equal to size=%d", pageOffset, size))
+	}
+	return buf
+}
+
+// A BlockReader holds metadata for accessing the columns of a columnar block.
+type BlockReader struct {
+	data             []byte
+	header           Header
+	customHeaderSize uint32
+}
+
+// ReadBlock decodes the header of the provided columnar block and returns a new
+// BlockReader configured to read from the block. The caller must ensure that
+// the data is formatted as to the block layout specification.
+func ReadBlock(data []byte, customHeaderSize uint32) BlockReader {
+	r := BlockReader{}
+	r.Init(data, customHeaderSize)
+	return r
+}
+
+// Init initializes a BlockReader with the data contained in the provided block.
+func (r *BlockReader) Init(data []byte, customHeaderSize uint32) {
+	*r = BlockReader{
+		data:             data,
+		header:           DecodeHeader(data[customHeaderSize:]),
+		customHeaderSize: customHeaderSize,
+	}
+}
+
+// DataType returns the data type of the col'th column. Every column's data type
+// is encoded within the block header.
+func (r *BlockReader) DataType(col int) DataType {
+	if uint16(col) >= r.header.Columns {
+		panic(errors.AssertionFailedf("column %d is out of range [0, %d)", col, r.header.Columns))
+	}
+	return DataType(*(*uint8)(r.pointer(r.customHeaderSize + 7 + 5*uint32(col))))
+}
+
+// Bitmap retrieves the col'th column as a bitmap. The column must be of type
+// DataTypeBool.
+func (r *BlockReader) Bitmap(col int) Bitmap {
+	if dt := r.DataType(col); dt != DataTypeBool {
+		panic(errors.AssertionFailedf("column %d is not a Bitmap; holds data type %s", dt))
+	}
+	return MakeBitmap(r.data, r.pageStart(col), int(r.header.Rows))
+}
+
+// RawBytes retrieves the col'th column as a column of byte slices. The column
+// must be of type DataTypeBytes.
+func (r *BlockReader) RawBytes(col int) RawBytes {
+	if dt := r.DataType(col); dt != DataTypeBytes {
+		panic(errors.AssertionFailedf("column %d is not a RawBytes column; holds data type %s", dt))
+	}
+	return MakeRawBytes(int(r.header.Rows), r.data, r.pageStart(col))
+}
+
+// Uint8s retrieves the col'th column as a column of uint8s. The column must be
+// of type DataTypeUint8.
+func (r *BlockReader) Uint8s(col int) UnsafeUint8s {
+	if dt := r.DataType(col); dt != DataTypeUint8 {
+		panic(errors.AssertionFailedf("column %d is not a Uint8 column; holds data type %s", col, dt))
+	}
+	_, s := readUnsafeIntegerSlice[uint8](int(r.header.Rows), r.data, r.pageStart(col))
+	return s
+}
+
+// Uint16s retrieves the col'th column as a column of uint8s. The column must be
+// of type DataTypeUint16.
+func (r *BlockReader) Uint16s(col int) UnsafeUint16s {
+	if dt := r.DataType(col); dt != DataTypeUint16 {
+		panic(errors.AssertionFailedf("column %d is not a Uint16 column; holds data type %s", col, dt))
+	}
+	_, s := readUnsafeIntegerSlice[uint16](int(r.header.Rows), r.data, r.pageStart(col))
+	return s
+}
+
+// Uint32s retrieves the col'th column as a column of uint32s. The column must be
+// of type DataTypeUint32.
+func (r *BlockReader) Uint32s(col int) UnsafeUint32s {
+	if dt := r.DataType(col); dt != DataTypeUint32 {
+		panic(errors.AssertionFailedf("column %d is not a Uint32 column; holds data type %s", col, dt))
+	}
+	_, s := readUnsafeIntegerSlice[uint32](int(r.header.Rows), r.data, r.pageStart(col))
+	return s
+}
+
+// Uint64s retrieves the col'th column as a column of uint64s. The column must be
+// of type DataTypeUint64.
+func (r *BlockReader) Uint64s(col int) UnsafeUint64s {
+	if dt := r.DataType(col); dt != DataTypeUint64 {
+		panic(errors.AssertionFailedf("column %d is not a Uint64 column; holds data type %s", col, dt))
+	}
+	_, s := readUnsafeIntegerSlice[uint64](int(r.header.Rows), r.data, r.pageStart(col))
+	return s
+}
+
+func (r *BlockReader) pageStart(col int) uint32 {
+	if uint16(col) >= r.header.Columns {
+		// -1 for the trailing version byte
+		return uint32(len(r.data) - 1)
+	}
+	return binary.LittleEndian.Uint32(
+		unsafe.Slice((*byte)(unsafe.Pointer(r.pointer(r.customHeaderSize+uint32(blockHeaderBaseSize+columnHeaderSize*col+1)))), 4))
+}
+
+func (r *BlockReader) pointer(offset uint32) unsafe.Pointer {
+	return unsafe.Pointer(uintptr(unsafe.Pointer(&r.data[0])) + uintptr(offset))
+}
+
+// FormattedString returns a formatted representation of the block's binary
+// data.
+func (r *BlockReader) FormattedString() string {
+	f := binfmt.New(r.data)
+	r.headerToBinFormatter(f)
+	for i := 0; i < int(r.header.Columns); i++ {
+		r.columnToBinFormatter(f, i, int(r.header.Rows))
+	}
+	f.HexBytesln(1, "block trailer padding")
+	return f.String()
+}
+
+func (r *BlockReader) headerToBinFormatter(f *binfmt.Formatter) {
+	f.CommentLine("columnar block header")
+	f.HexBytesln(1, "version %v", Version(f.PeekUint(1)))
+	f.HexBytesln(2, "%d columns", r.header.Columns)
+	f.HexBytesln(4, "%d rows", r.header.Rows)
+	for i := 0; i < int(r.header.Columns); i++ {
+		f.CommentLine("column %d", i)
+		f.Byte("%s", r.DataType(i))
+		f.HexBytesln(4, "page start %d", r.pageStart(i))
+	}
+}
+
+func (r *BlockReader) columnToBinFormatter(f *binfmt.Formatter, col, rows int) {
+	f.CommentLine("data for column %d", col)
+	dataType := r.DataType(col)
+	colSize := r.pageStart(col+1) - r.pageStart(col)
+	endOff := f.Offset() + int(colSize)
+	switch dataType {
+	case DataTypeBool:
+		bitmapToBinFormatter(f, rows)
+	case DataTypeUint8, DataTypeUint16, DataTypeUint32, DataTypeUint64:
+		uintsToBinFormatter(f, rows, dataType, nil)
+	case DataTypeBytes:
+		rawBytesToBinFormatter(f, rows, nil)
+	default:
+		panic("unimplemented")
+	}
+
+	// We expect formatting the column data to have consumed all the bytes
+	// between the column's pageOffset and the next column's pageOffset.
+	switch v := endOff - f.Offset(); cmp.Compare[int](v, 0) {
+	case +1:
+		panic(fmt.Sprintf("expected f.Offset() = %d, but found %d; did column %s format too few bytes?", endOff, f.Offset(), dataType))
+	case 0:
+	case -1:
+		panic(fmt.Sprintf("expected f.Offset() = %d, but found %d; did column %s format too many bytes?", endOff, f.Offset(), dataType))
+	}
+}
diff --git a/sstable/colblk/column.go b/sstable/colblk/column.go
index 6e4967c7..e715d060 100644
--- a/sstable/colblk/column.go
+++ b/sstable/colblk/column.go
@@ -4,12 +4,7 @@
 
 package colblk
 
-import (
-	"bytes"
-	"fmt"
-	"io"
-	"strings"
-)
+import "io"
 
 // DataType describes the logical type of a column's values. Some data types
 // have multiple possible physical representations. Encoding a column may choose
@@ -67,152 +62,14 @@ func (t DataType) uintWidth() uint32 {
 	panic("not a unit")
 }
 
-// ColumnDesc describes the column's data type and its encoding.
-type ColumnDesc struct {
-	DataType DataType
-	Encoding ColumnEncoding
-}
-
-// TODO(jackson): Ensure we provide a mechanism for future extensibility: maybe
-// a single byte to represent the colblk version?
-
-// String returns a human-readable string describing the column encoding.
-func (d ColumnDesc) String() string {
-	var sb strings.Builder
-	fmt.Fprint(&sb, d.DataType.String())
-	if d.Encoding != EncodingDefault {
-		fmt.Fprintf(&sb, "+%s", d.Encoding)
-	}
-	return sb.String()
-}
-
-// ColumnDescs is a slice of ColumnDesc values.
-type ColumnDescs []ColumnDesc
-
-// String returns the concatenated string representations of the column
-// descriptions.
-func (c ColumnDescs) String() string {
-	var buf bytes.Buffer
-	for i := range c {
-		if i > 0 {
-			buf.WriteString(" ")
-		}
-		buf.WriteString(c[i].String())
-	}
-	return buf.String()
-}
-
-// ColumnEncoding describes the encoding of a column.
-//
-// Column data can optionally contain a NULL bitmap, where each bit corresponds
-// to a row in the column and a set bit indicates that the corresponding row is
-// NULL. Column data can optionally use a delta encoding where each value is a
-// delta from a base, constant value.
-//
-// The layout of the encoded data is as follows:
-//   - [optional] NULL bitmap
-//   - [optional] a "base" constant value for delta encodings
-//   - n data values
-//
-// The ColumnEncoding byte is a combination of the following values:
-//   - The bits 0,1,2 encode the DeltaEncoding enum variant.
-//   - The bit 3 is set if the column has a NULL bitmap.
-//
-// The delta encoding is currently only used for uint columns.
-type ColumnEncoding uint8
-
-const (
-	// EncodingDefault indicates that the default encoding is in-use for a
-	// column, encoding n values for n rows. In EncodingDefault, no null bitmap
-	// is set, no delta encoding is used, etc.
-	EncodingDefault ColumnEncoding = 0
-
-	encodingUintDeltaMask        = 0b00000111
-	encodingUintDeltaInverseMask = 0b11111000
-	encodingNullBitmapBit        = 0b00001000
-
-	// TODO(jackson): Add additional encoding types.
-)
-
-// Delta returns the delta encoding of the column.
-func (e ColumnEncoding) Delta() DeltaEncoding {
-	return DeltaEncoding(e & encodingUintDeltaMask)
-}
-
-// WithDelta returns the column encoding with the provided delta encoding.
-func (e ColumnEncoding) WithDelta(d DeltaEncoding) ColumnEncoding {
-	return (e & encodingUintDeltaInverseMask) | ColumnEncoding(d)
-}
-
-// String returns the string representation of the column encoding.
-func (e ColumnEncoding) String() string {
-	var sb strings.Builder
-	fmt.Fprint(&sb, e.Delta().String())
-	return sb.String()
-}
-
-// DeltaEncoding indicates what delta encoding, if any is in use by a column to
-// reduce the per-row storage size.
-//
-// A uint delta encoding represents every non-NULL element in an array of uints
-// as a delta relative to the column's constant. The logical value of each row
-// is computed as C + D[i] where C is the column constant and D[i] is the delta.
-type DeltaEncoding uint8
-
-const (
-	// DeltaEncodingNone indicates no delta encoding is in use. N rows are
-	// represented using N values of the column's logical data type.
-	DeltaEncodingNone DeltaEncoding = 0
-	// DeltaEncodingConstant indicates that all rows of the column share the
-	// same value. The column data encodes the constant value and no deltas.
-	DeltaEncodingConstant DeltaEncoding = 1
-	// DeltaEncodingUint8 indicates each delta is represented as a 1-byte uint8.
-	DeltaEncodingUint8 DeltaEncoding = 2
-	// DeltaEncodingUint16 indicates each delta is represented as a 2-byte uint16.
-	DeltaEncodingUint16 DeltaEncoding = 3
-	// DeltaEncodingUint32 indicates each delta is represented as a 4-byte uint32.
-	DeltaEncodingUint32 DeltaEncoding = 4
-)
-
-// String implements fmt.Stringer.
-func (d DeltaEncoding) String() string {
-	switch d {
-	case DeltaEncodingNone:
-		return "none"
-	case DeltaEncodingConstant:
-		return "const"
-	case DeltaEncodingUint8:
-		return "delta8"
-	case DeltaEncodingUint16:
-		return "delta16"
-	case DeltaEncodingUint32:
-		return "delta32"
-	default:
-		panic("unreachable")
-	}
-}
-
-func (d DeltaEncoding) width() int {
-	switch d {
-	case DeltaEncodingConstant:
-		return 0
-	case DeltaEncodingUint8:
-		return 1
-	case DeltaEncodingUint16:
-		return 2
-	case DeltaEncodingUint32:
-		return 4
-	default:
-		panic("unreachable")
-	}
-}
-
 // ColumnWriter is an interface implemented by column encoders that accumulate a
 // column's values and then serialize them.
 type ColumnWriter interface {
 	Encoder
 	// NumColumns returns the number of columns the ColumnWriter will encode.
 	NumColumns() int
+	// DataType returns the data type of the col'th column.
+	DataType(col int) DataType
 	// Finish serializes the column at the specified index, writing the column's
 	// data to buf at offset, and returning the offset at which the next column
 	// should be encoded. Finish also returns a column descriptor describing the
@@ -229,7 +86,7 @@ type ColumnWriter interface {
 	// The provided buf must be word-aligned (at offset 0). If a column writer
 	// requires a particularly alignment, it's responsible for padding offset
 	// appropriately first.
-	Finish(col int, rows int, offset uint32, buf []byte) (nextOffset uint32, desc ColumnDesc)
+	Finish(col, rows int, offset uint32, buf []byte) (nextOffset uint32)
 }
 
 // Encoder is an interface implemented by column encoders.
diff --git a/sstable/colblk/raw_bytes.go b/sstable/colblk/raw_bytes.go
index d26f5385..7cb0c6f7 100644
--- a/sstable/colblk/raw_bytes.go
+++ b/sstable/colblk/raw_bytes.go
@@ -51,11 +51,11 @@ type RawBytes struct {
 
 // MakeRawBytes constructs an accessor for an array of byte slices constructed
 // by RawBytesBuilder. Count must be the number of byte slices within the array.
-func MakeRawBytes(count int, b []byte, offset uint32, enc ColumnEncoding) RawBytes {
+func MakeRawBytes(count int, b []byte, offset uint32) RawBytes {
 	if count == 0 {
 		return RawBytes{}
 	}
-	dataOff, offsets := readUnsafeIntegerSlice[uint32](count+1 /* +1 offset */, b, offset, enc.Delta())
+	dataOff, offsets := readUnsafeIntegerSlice[uint32](count+1 /* +1 offset */, b, offset)
 	return RawBytes{
 		slices:  count,
 		offsets: offsets,
@@ -68,21 +68,19 @@ func defaultSliceFormatter(x []byte) string {
 	return string(x)
 }
 
-func rawBytesToBinFormatter(
-	f *binfmt.Formatter, count int, enc ColumnEncoding, sliceFormatter func([]byte) string,
-) {
+func rawBytesToBinFormatter(f *binfmt.Formatter, count int, sliceFormatter func([]byte) string) {
 	if sliceFormatter == nil {
 		sliceFormatter = defaultSliceFormatter
 	}
 
-	rb := MakeRawBytes(count, f.Data(), uint32(f.Offset()), enc)
+	rb := MakeRawBytes(count, f.Data(), uint32(f.Offset()))
 	dataOffset := uint64(f.Offset()) + uint64(uintptr(rb.data)-uintptr(rb.start))
-	f.CommentLine("RawBytes")
-	f.CommentLine("Offsets table")
-	uintsToBinFormatter(f, count+1, ColumnDesc{DataType: DataTypeUint32, Encoding: enc}, func(offset uint64) string {
+	f.CommentLine("rawbytes")
+	f.CommentLine("offsets table")
+	uintsToBinFormatter(f, count+1, DataTypeUint32, func(offset uint64) string {
 		return fmt.Sprintf("%d [%d overall]", offset, offset+dataOffset)
 	})
-	f.CommentLine("Data")
+	f.CommentLine("data")
 	for i := 0; i < rb.slices; i++ {
 		s := rb.At(i)
 		f.HexBytesln(len(s), "data[%d]: %s", i, sliceFormatter(s))
@@ -131,6 +129,9 @@ func (b *RawBytesBuilder) Reset() {
 // NumColumns implements ColumnWriter.
 func (b *RawBytesBuilder) NumColumns() int { return 1 }
 
+// DataType implements ColumnWriter.
+func (b *RawBytesBuilder) DataType(int) DataType { return DataTypeBytes }
+
 // Put appends the provided byte slice to the builder.
 func (b *RawBytesBuilder) Put(s []byte) {
 	b.data = append(b.data, s...)
@@ -149,19 +150,14 @@ func (b *RawBytesBuilder) PutConcat(s1, s2 []byte) {
 // Finish writes the serialized byte slices to buf starting at offset. The buf
 // slice must be sufficiently large to store the serialized output. The caller
 // should use [Size] to size buf appropriately before calling Finish.
-func (b *RawBytesBuilder) Finish(
-	col int, rows int, offset uint32, buf []byte,
-) (uint32, ColumnDesc) {
-	desc := ColumnDesc{DataType: DataTypeBytes, Encoding: EncodingDefault}
+func (b *RawBytesBuilder) Finish(col, rows int, offset uint32, buf []byte) uint32 {
 	if rows == 0 {
-		return 0, desc
+		return 0
 	}
 	dataLen := b.offsets.Get(rows)
-	dataOffset, offsetColumnDesc := b.offsets.Finish(0, rows+1, offset, buf)
-	desc.Encoding = offsetColumnDesc.Encoding
+	offset = b.offsets.Finish(0, rows+1, offset, buf)
 	// Copy the data section.
-	endOffset := dataOffset + uint32(copy(buf[dataOffset:], b.data[:dataLen]))
-	return endOffset, desc
+	return offset + uint32(copy(buf[offset:], b.data[:dataLen]))
 }
 
 // Size computes the size required to encode the byte slices beginning in a
diff --git a/sstable/colblk/uints.go b/sstable/colblk/uints.go
index 5ff86e14..5fc8c915 100644
--- a/sstable/colblk/uints.go
+++ b/sstable/colblk/uints.go
@@ -21,6 +21,65 @@ type Uint interface {
 	~uint8 | ~uint16 | ~uint32 | ~uint64
 }
 
+// UintDeltaEncoding indicates what delta encoding, if any is in use by a
+// uint{8,16,32,64} column to reduce the per-row storage size.
+//
+// A uint delta encoding represents every non-NULL element in an array of uints
+// as a delta relative to the column's constant. The logical value of each row
+// is computed as C + D[i] where C is the column constant and D[i] is the delta.
+//
+// The UintDeltaEncoding byte is serialized to the uint column before the column
+// data.
+type UintDeltaEncoding uint8
+
+const (
+	// UintDeltaEncodingNone indicates no delta encoding is in use. N rows are
+	// represented using N values of the column's logical data type.
+	UintDeltaEncodingNone UintDeltaEncoding = 0
+	// UintDeltaEncodingConstant indicates that all rows of the column share the
+	// same value. The column data encodes the constant value and no deltas.
+	UintDeltaEncodingConstant UintDeltaEncoding = 1
+	// UintDeltaEncoding8 indicates each delta is represented as a 1-byte uint8.
+	UintDeltaEncoding8 UintDeltaEncoding = 2
+	// UintDeltaEncoding16 indicates each delta is represented as a 2-byte uint16.
+	UintDeltaEncoding16 UintDeltaEncoding = 3
+	// UintDeltaEncoding32 indicates each delta is represented as a 4-byte uint32.
+	UintDeltaEncoding32 UintDeltaEncoding = 4
+)
+
+// String implements fmt.Stringer.
+func (d UintDeltaEncoding) String() string {
+	switch d {
+	case UintDeltaEncodingNone:
+		return "none"
+	case UintDeltaEncodingConstant:
+		return "const"
+	case UintDeltaEncoding8:
+		return "delta8"
+	case UintDeltaEncoding16:
+		return "delta16"
+	case UintDeltaEncoding32:
+		return "delta32"
+	default:
+		panic("unreachable")
+	}
+}
+
+func (d UintDeltaEncoding) width() int {
+	switch d {
+	case UintDeltaEncodingConstant:
+		return 0
+	case UintDeltaEncoding8:
+		return 1
+	case UintDeltaEncoding16:
+		return 2
+	case UintDeltaEncoding32:
+		return 4
+	default:
+		panic("unreachable")
+	}
+}
+
 // UintBuilder builds a column of unsigned integers of the same width.
 // UintBuilder uses a delta encoding when possible to store values using
 // lower-width integers. See DeltaEncoding.
@@ -107,6 +166,9 @@ func (b *UintBuilder[T]) init(useDefault bool) {
 // NumColumns implements ColumnWriter.
 func (b *UintBuilder[T]) NumColumns() int { return 1 }
 
+// DataType implements ColumnWriter.
+func (b *UintBuilder[T]) DataType(int) DataType { return b.dt }
+
 // Reset implements ColumnWriter and resets the builder, reusing existing
 // allocated memory.
 func (b *UintBuilder[T]) Reset() {
@@ -198,6 +260,7 @@ func (b *UintBuilder[T]) Size(rows int, offset uint32) uint32 {
 // encoded at the provided offset using the provided width. If width <
 // sizeof(T), then a delta encoding is assumed.
 func uintColumnSize[T Uint](rows, offset, width uint32) uint32 {
+	offset++ // DeltaEncoding byte
 	logicalWidth := uint32(unsafe.Sizeof(T(0)))
 	if width != logicalWidth {
 		// A delta encoding will be used. We need to first account for the constant
@@ -216,10 +279,9 @@ func uintColumnSize[T Uint](rows, offset, width uint32) uint32 {
 
 // Finish implements ColumnWriter, serializing the column into offset [offset] of
 // [buf].
-func (b *UintBuilder[T]) Finish(col, rows int, offset uint32, buf []byte) (uint32, ColumnDesc) {
-	desc := ColumnDesc{DataType: b.dt}
+func (b *UintBuilder[T]) Finish(col, rows int, offset uint32, buf []byte) uint32 {
 	if rows == 0 {
-		return offset, desc
+		return offset
 	}
 
 	// Determine the width of each element with delta-encoding applied.
@@ -235,59 +297,61 @@ func (b *UintBuilder[T]) Finish(col, rows int, offset uint32, buf []byte) (uint3
 		minimum, maximum = computeMinMax(b.array.elems.Slice(rows))
 		w = deltaWidth(uint64(maximum - minimum))
 	}
-	offset, desc.Encoding = uintColumnFinish[T](minimum, b.array.elems.Slice(rows), w, offset, buf)
-	return offset, desc
+	return uintColumnFinish[T](minimum, b.array.elems.Slice(rows), w, offset, buf)
 }
 
 // uintColumnFinish finishes the column of unsigned integers of type T, encoding
 // per-row deltas of size width if width < sizeof(T).
-func uintColumnFinish[T Uint](
-	minimum T, values []T, width, offset uint32, buf []byte,
-) (uint32, ColumnEncoding) {
-	enc := EncodingDefault
-
+func uintColumnFinish[T Uint](minimum T, values []T, width, offset uint32, buf []byte) uint32 {
 	// Compare the computed delta width to see if we're able to use an array of
 	// lower-width deltas to encode the column.
 	if uintptr(width) < unsafe.Sizeof(T(0)) {
-		// Regardless of the width, we encode a constant of size T encoding the
-		// minimum across all the values.
-		offset += uint32(writeLittleEndianNonaligned(buf, offset, minimum))
-
 		switch width {
 		case 0:
 			// All the column values are the same and we can elide any deltas at
 			// all.
-			enc = enc.WithDelta(DeltaEncodingConstant)
-			return offset, enc
+			buf[offset] = byte(UintDeltaEncodingConstant)
+			offset++
+			offset += uint32(writeLittleEndianNonaligned(buf, offset, minimum))
+			return offset
 		case 1:
-			enc = enc.WithDelta(DeltaEncodingUint8)
+			buf[offset] = byte(UintDeltaEncoding8)
+			offset++
+			offset += uint32(writeLittleEndianNonaligned(buf, offset, minimum))
 			dest := makeUnsafeRawSlice[uint8](unsafe.Pointer(&buf[offset]))
 			reduceUints[T, uint8](minimum, values, dest.Slice(len(values)))
 			offset += uint32(len(values))
-			return offset, enc
+			return offset
 		case align16:
-			enc = enc.WithDelta(DeltaEncodingUint16)
+			buf[offset] = byte(UintDeltaEncoding16)
+			offset++
+			offset += uint32(writeLittleEndianNonaligned(buf, offset, minimum))
 			// Align the offset appropriately for uint16s.
 			offset = alignWithZeroes(buf, offset, align16)
 			dest := makeUnsafeRawSlice[uint16](unsafe.Pointer(&buf[offset]))
 			reduceUints[T, uint16](minimum, values, dest.Slice(len(values)))
 			offset += uint32(len(values)) * align16
-			return offset, enc
+			return offset
 		case align32:
-			enc = enc.WithDelta(DeltaEncodingUint32)
+			buf[offset] = byte(UintDeltaEncoding32)
+			offset++
+			offset += uint32(writeLittleEndianNonaligned(buf, offset, minimum))
 			// Align the offset appropriately for uint32s.
 			offset = alignWithZeroes(buf, offset, align32)
 			dest := makeUnsafeRawSlice[uint32](unsafe.Pointer(&buf[offset]))
 			reduceUints[T, uint32](minimum, values, dest.Slice(len(values)))
 			offset += uint32(len(values)) * align32
-			return offset, enc
+			return offset
 		default:
 			panic("unreachable")
 		}
 	}
+	buf[offset] = byte(UintDeltaEncodingNone)
+	offset++
+	offset = align(offset, uint32(unsafe.Sizeof(T(0))))
 	dest := makeUnsafeRawSlice[T](unsafe.Pointer(&buf[offset])).Slice(len(values))
 	offset += uint32(copy(dest, values)) * uint32(unsafe.Sizeof(T(0)))
-	return offset, enc
+	return offset
 }
 
 // writeLittleEndianNonaligned writes v to buf at the provided offset in
@@ -378,34 +442,37 @@ func deltaWidth(delta uint64) uint32 {
 }
 
 func uintsToBinFormatter(
-	f *binfmt.Formatter, rows int, desc ColumnDesc, uintFormatter func(uint64) string,
+	f *binfmt.Formatter, rows int, dataType DataType, uintFormatter func(uint64) string,
 ) {
 	if uintFormatter == nil {
 		uintFormatter = func(v uint64) string { return fmt.Sprint(v) }
 	}
 
-	logicalWidth := desc.DataType.uintWidth()
+	deltaEncoding := UintDeltaEncoding(f.PeekUint(1)) // DeltaEncoding byte
+	f.HexBytesln(1, "delta encoding: %s", deltaEncoding)
+
+	logicalWidth := dataType.uintWidth()
 
 	elementWidth := int(logicalWidth)
-	if desc.Encoding.Delta() != DeltaEncodingNone {
+	if deltaEncoding != UintDeltaEncodingNone {
 		f.HexBytesln(int(logicalWidth), "%d-bit constant: %d", logicalWidth*8, f.PeekUint(int(logicalWidth)))
 
-		switch desc.Encoding.Delta() {
-		case DeltaEncodingConstant:
+		switch deltaEncoding {
+		case UintDeltaEncodingConstant:
 			// This is just a constant (that was already read/formatted).
-			rows = 0
-		case DeltaEncodingUint8:
+			return
+		case UintDeltaEncoding8:
 			elementWidth = 1
-		case DeltaEncodingUint16:
+		case UintDeltaEncoding16:
 			elementWidth = align16
-		case DeltaEncodingUint32:
+		case UintDeltaEncoding32:
 			elementWidth = align32
 		default:
 			panic("unreachable")
 		}
 	}
 	if off := align(f.Offset(), int(elementWidth)); off != f.Offset() {
-		f.CommentLine("Padding")
+		f.CommentLine("padding")
 		f.HexBytesln(off-f.Offset(), "aligning to %d-bit boundary", elementWidth*8)
 	}
 	for i := 0; i < rows; i++ {
diff --git a/sstable/colblk/unsafe_slice.go b/sstable/colblk/unsafe_slice.go
index b0eddee5..3ab6851c 100644
--- a/sstable/colblk/unsafe_slice.go
+++ b/sstable/colblk/unsafe_slice.go
@@ -68,18 +68,20 @@ type UnsafeIntegerSlice[T constraints.Integer] struct {
 }
 
 func readUnsafeIntegerSlice[T constraints.Integer](
-	rows int, b []byte, off uint32, delta DeltaEncoding,
+	rows int, b []byte, off uint32,
 ) (endOffset uint32, slice UnsafeIntegerSlice[T]) {
+	delta := UintDeltaEncoding(b[off])
+	off++
 	switch delta {
-	case DeltaEncodingNone:
+	case UintDeltaEncodingNone:
 		off = align(off, uint32(unsafe.Sizeof(T(0))))
 		slice = makeUnsafeIntegerSlice[T](0, unsafe.Pointer(&b[off]), int(unsafe.Sizeof(T(0))))
 		off += uint32(unsafe.Sizeof(T(0))) * uint32(rows)
-	case DeltaEncodingConstant:
+	case UintDeltaEncodingConstant:
 		base := readLittleEndianNonaligned[T](b, off)
 		off += uint32(unsafe.Sizeof(T(0)))
 		slice = makeUnsafeIntegerSlice[T](base, unsafe.Pointer(&b[off]), 0)
-	case DeltaEncodingUint8, DeltaEncodingUint16, DeltaEncodingUint32:
+	case UintDeltaEncoding8, UintDeltaEncoding16, UintDeltaEncoding32:
 		w := delta.width()
 		base := readLittleEndianNonaligned[T](b, off)
 		off += uint32(unsafe.Sizeof(T(0)))


Test patch:
diff --git a/sstable/colblk/bitmap_test.go b/sstable/colblk/bitmap_test.go
index 82d7430f..f3da1c3f 100644
--- a/sstable/colblk/bitmap_test.go
+++ b/sstable/colblk/bitmap_test.go
@@ -42,8 +42,8 @@ func TestBitmapFixed(t *testing.T) {
 				builder.Invert(n)
 			}
 
-			_, _ = builder.Finish(0, n, 0, data)
-			bitmap = MakeBitmap(data, n)
+			_ = builder.Finish(0, n, 0, data)
+			bitmap = MakeBitmap(data, 0, n)
 			dumpBitmap(&buf, bitmap)
 			fmt.Fprint(&buf, "\nBinary representation:\n")
 			f := binfmt.New(data)
@@ -99,8 +99,8 @@ func TestBitmapRandom(t *testing.T) {
 			}
 		}
 		data := make([]byte, builder.Size(size, 0))
-		_, _ = builder.Finish(0, size, 0, data)
-		bitmap := MakeBitmap(data, size)
+		_ = builder.Finish(0, size, 0, data)
+		bitmap := MakeBitmap(data, 0, size)
 		for i := 0; i < size; i++ {
 			if got := bitmap.Get(i); got != v[i] {
 				t.Fatalf("b.Get(%d) = %t; want %t", i, got, v[i])
@@ -164,6 +164,6 @@ func BenchmarkBitmapBuilder(b *testing.B) {
 				builder.Set(i, v[i])
 			}
 		}
-		_, _ = builder.Finish(0, size, 0, data)
+		_ = builder.Finish(0, size, 0, data)
 	}
 }
diff --git a/sstable/colblk/block_test.go b/sstable/colblk/block_test.go
new file mode 100644
index 00000000..1655ee48
--- /dev/null
+++ b/sstable/colblk/block_test.go
@@ -0,0 +1,349 @@
+// Copyright 2024 The LevelDB-Go and Pebble Authors. All rights reserved. Use
+// of this source code is governed by a BSD-style license that can be found in
+// the LICENSE file.
+
+package colblk
+
+import (
+	"bytes"
+	"fmt"
+	"reflect"
+	"slices"
+	"strconv"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/cockroachdb/datadriven"
+	"golang.org/x/exp/rand"
+)
+
+func TestBlockWriter(t *testing.T) {
+	panicIfErr := func(dataType DataType, stringValue string, err error) {
+		if err != nil {
+			panic(fmt.Sprintf("unable to decode %q as value for data type %s: %s", stringValue, dataType, err))
+		}
+	}
+	var buf bytes.Buffer
+	var rows int
+	var colDataTypes []DataType
+	var colWriters []ColumnWriter
+	datadriven.RunTest(t, "testdata/block_writer", func(t *testing.T, td *datadriven.TestData) string {
+		buf.Reset()
+		switch td.Cmd {
+		case "init":
+			colTypeNames, ok := td.Arg("schema")
+			if !ok {
+				return "schema argument missing"
+			}
+			colDataTypes = make([]DataType, len(colTypeNames.Vals))
+			colWriters = make([]ColumnWriter, len(colDataTypes))
+			for i, v := range colTypeNames.Vals {
+				colDataTypes[i] = dataTypeFromName(v)
+				switch colDataTypes[i] {
+				case DataTypeBool:
+					colWriters[i] = &BitmapBuilder{}
+				case DataTypeUint8:
+					b := &UintBuilder[uint8]{}
+					b.Init()
+					colWriters[i] = b
+				case DataTypeUint16:
+					b := &UintBuilder[uint16]{}
+					b.Init()
+					colWriters[i] = b
+				case DataTypeUint32:
+					b := &UintBuilder[uint32]{}
+					b.Init()
+					colWriters[i] = b
+				case DataTypeUint64:
+					b := &UintBuilder[uint64]{}
+					b.Init()
+					colWriters[i] = b
+				case DataTypeBytes:
+					bb := &RawBytesBuilder{}
+					bb.Reset()
+					colWriters[i] = bb
+				case DataTypePrefixBytes:
+					panic("unimplemented")
+				default:
+					panic(fmt.Sprintf("unsupported data type: %s", v))
+				}
+				colWriters[i].Reset()
+			}
+			rows = 0
+			return ""
+		case "write":
+			lines := strings.Split(td.Input, "\n")
+			lineFields := make([][]string, len(lines))
+			for i, line := range lines {
+				lineFields[i] = strings.Fields(line)
+			}
+			rows += len(lineFields)
+			for c, dataType := range colDataTypes {
+				switch dataType {
+				case DataTypeBool:
+					bb := colWriters[c].(*BitmapBuilder)
+					for r := range lineFields {
+						v, err := strconv.ParseBool(lineFields[r][c])
+						panicIfErr(dataType, lineFields[r][c], err)
+						bb.Set(r, v)
+					}
+				case DataTypeUint8:
+					b := colWriters[c].(*UintBuilder[uint8])
+					for r := range lineFields {
+						v, err := strconv.ParseUint(lineFields[r][c], 10, 8)
+						panicIfErr(dataType, lineFields[r][c], err)
+						b.Set(r, uint8(v))
+					}
+				case DataTypeUint16:
+					b := colWriters[c].(*UintBuilder[uint16])
+					for r := range lineFields {
+						v, err := strconv.ParseUint(lineFields[r][c], 10, 16)
+						panicIfErr(dataType, lineFields[r][c], err)
+						b.Set(r, uint16(v))
+					}
+				case DataTypeUint32:
+					b := colWriters[c].(*UintBuilder[uint32])
+					for r := range lineFields {
+						v, err := strconv.ParseUint(lineFields[r][c], 10, 32)
+						panicIfErr(dataType, lineFields[r][c], err)
+						b.Set(r, uint32(v))
+					}
+				case DataTypeUint64:
+					b := colWriters[c].(*UintBuilder[uint64])
+					for r := range lineFields {
+						v, err := strconv.ParseUint(lineFields[r][c], 10, 64)
+						panicIfErr(dataType, lineFields[r][c], err)
+						b.Set(r, v)
+					}
+				case DataTypeBytes:
+					b := colWriters[c].(*RawBytesBuilder)
+					for r := range lineFields {
+						b.Put([]byte(lineFields[r][c]))
+					}
+				case DataTypePrefixBytes:
+					panic("unimplemented")
+				default:
+					panic(fmt.Sprintf("unsupported data type: %s", dataType))
+				}
+			}
+			return ""
+		case "finish":
+			block := FinishBlock(int(rows), colWriters)
+			r := ReadBlock(block, 0)
+			return r.FormattedString()
+		default:
+			return fmt.Sprintf("unknown command: %s", td.Cmd)
+		}
+	})
+}
+
+func dataTypeFromName(name string) DataType {
+	for dt, n := range dataTypeName {
+		if n == name {
+			return DataType(dt)
+		}
+	}
+	return DataTypeInvalid
+}
+
+// randBlock generates a random block of n rows with the provided schema. It
+// returns the serialized raw block and a []interface{} slice containing the
+// generated data. The type of each element of the slice is dependent on the
+// corresponding column's type.
+func randBlock(rng *rand.Rand, rows int, schema []DataType) ([]byte, []interface{}) {
+	data := make([]interface{}, len(schema))
+	for col := range data {
+		switch schema[col] {
+		case DataTypeBool:
+			v := make([]bool, rows)
+			for row := 0; row < rows; row++ {
+				v[row] = (rng.Int31() % 2) == 0
+			}
+			data[col] = v
+		case DataTypeUint8:
+			v := make([]uint8, rows)
+			for row := 0; row < rows; row++ {
+				v[row] = uint8(rng.Uint32())
+			}
+			data[col] = v
+		case DataTypeUint16:
+			v := make([]uint16, rows)
+			for row := 0; row < rows; row++ {
+				v[row] = uint16(rng.Uint32())
+			}
+			data[col] = v
+		case DataTypeUint32:
+			v := make([]uint32, rows)
+			for row := 0; row < rows; row++ {
+				v[row] = rng.Uint32()
+			}
+			data[col] = v
+		case DataTypeUint64:
+			v := make([]uint64, rows)
+			for row := 0; row < rows; row++ {
+				v[row] = rng.Uint64()
+			}
+			data[col] = v
+		case DataTypeBytes:
+			v := make([][]byte, rows)
+			for row := 0; row < rows; row++ {
+				v[row] = make([]byte, rng.Intn(20))
+				rng.Read(v[row])
+			}
+			data[col] = v
+		case DataTypePrefixBytes:
+			v := make([][]byte, rows)
+			for row := 0; row < rows; row++ {
+				v[row] = make([]byte, rng.Intn(20))
+				rng.Read(v[row])
+			}
+			// PrefixBytes are required to be lexicographically sorted.
+			slices.SortFunc(v, bytes.Compare)
+			data[col] = v
+
+		}
+	}
+	buf := buildBlock(schema, rows, data)
+	return buf, data
+}
+
+func buildBlock(schema []DataType, rows int, data []interface{}) []byte {
+	cw := make([]ColumnWriter, len(schema))
+	for col := range schema {
+		switch schema[col] {
+		case DataTypeBool:
+			var bb BitmapBuilder
+			bb.Reset()
+			for row, v := range data[col].([]bool) {
+				bb.Set(row, v)
+			}
+			cw[col] = &bb
+		case DataTypeUint8:
+			var b UintBuilder[uint8]
+			b.Init()
+			for row, v := range data[col].([]uint8) {
+				b.Set(row, v)
+			}
+			cw[col] = &b
+		case DataTypeUint16:
+			var b UintBuilder[uint16]
+			b.Init()
+			for row, v := range data[col].([]uint16) {
+				b.Set(row, v)
+			}
+			cw[col] = &b
+		case DataTypeUint32:
+			var b UintBuilder[uint32]
+			b.Init()
+			for row, v := range data[col].([]uint32) {
+				b.Set(row, v)
+			}
+			cw[col] = &b
+		case DataTypeUint64:
+			var b UintBuilder[uint64]
+			b.Init()
+			for row, v := range data[col].([]uint64) {
+				b.Set(row, v)
+			}
+			cw[col] = &b
+		case DataTypeBytes:
+			var b RawBytesBuilder
+			b.Reset()
+			for _, v := range data[col].([][]byte) {
+				b.Put(v)
+			}
+			cw[col] = &b
+		case DataTypePrefixBytes:
+			panic("unimplemented")
+		}
+	}
+	return FinishBlock(rows, cw)
+}
+
+func testRandomBlock(t *testing.T, rng *rand.Rand, rows int, schema []DataType) {
+	var sb strings.Builder
+	for i := range schema {
+		if i > 0 {
+			sb.WriteByte(',')
+		}
+		sb.WriteString(schema[i].String())
+	}
+
+	t.Run(sb.String(), func(t *testing.T) {
+		block, data := randBlock(rng, rows, schema)
+		r := ReadBlock(block, 0)
+		if uint32(r.header.Columns) != uint32(len(schema)) {
+			t.Fatalf("expected %d columns, but found %d\n", len(schema), r.header.Columns)
+		}
+		if r.header.Rows != uint32(rows) {
+			t.Fatalf("expected %d rows, but found %d\n", rows, r.header.Rows)
+		}
+		for col := range schema {
+			if schema[col] != r.DataType(col) {
+				t.Fatalf("schema mismatch: %s != %s\n", schema[col], r.DataType(col))
+			}
+		}
+
+		for col := range data {
+			var got interface{}
+			switch schema[col] {
+			case DataTypeBool:
+				b := r.Bitmap(col)
+				vals := make([]bool, r.header.Rows)
+				for i := range vals {
+					vals[i] = b.Get(i)
+				}
+				got = vals
+			case DataTypeUint8:
+				got = r.Uint8s(col).Clone(rows)
+			case DataTypeUint16:
+				got = r.Uint16s(col).Clone(rows)
+			case DataTypeUint32:
+				got = r.Uint32s(col).Clone(rows)
+			case DataTypeUint64:
+				got = r.Uint64s(col).Clone(rows)
+			case DataTypeBytes:
+				vals2 := make([][]byte, rows)
+				vals := r.RawBytes(col)
+				for i := range vals2 {
+					vals2[i] = vals.At(i)
+				}
+				got = vals2
+			case DataTypePrefixBytes:
+				panic("unimplemented")
+			}
+			if !reflect.DeepEqual(data[col], got) {
+				t.Fatalf("%d: %s: expected\n%+v\ngot\n%+v\n% x",
+					col, schema[col], data[col], got, r.data)
+			}
+		}
+	})
+}
+
+func TestBlockWriterRandomized(t *testing.T) {
+	seed := uint64(time.Now().UnixNano())
+	t.Logf("Seed: %d", seed)
+	rng := rand.New(rand.NewSource(seed))
+	randInt := func(lo, hi int) int {
+		return lo + rng.Intn(hi-lo)
+	}
+	testRandomBlock(t, rng, randInt(1, 100), []DataType{DataTypeBool})
+	testRandomBlock(t, rng, randInt(1, 100), []DataType{DataTypeUint8})
+	testRandomBlock(t, rng, randInt(1, 100), []DataType{DataTypeUint16})
+	testRandomBlock(t, rng, randInt(1, 100), []DataType{DataTypeUint32})
+	testRandomBlock(t, rng, randInt(1, 100), []DataType{DataTypeUint64})
+	testRandomBlock(t, rng, randInt(1, 100), []DataType{DataTypeBytes})
+	// TODO(jackson): Add support for DataTypePrefixBytes.
+	// testRandomBlock(t, rng, randInt(1, 100), []DataType{DataTypePrefixBytes})
+
+	for i := 0; i < 100; i++ {
+		schema := make([]DataType, 2+rng.Intn(8))
+		for j := range schema {
+			// TODO(jackson): Adjust this to generate DataTypePrefixBytes
+			// columns too once they're supported.
+			schema[j] = DataType(randInt(1, int(DataTypePrefixBytes)-1))
+		}
+		testRandomBlock(t, rng, randInt(1, 100), schema)
+	}
+}
diff --git a/sstable/colblk/column_test.go b/sstable/colblk/column_test.go
deleted file mode 100644
index 8b0b61f8..00000000
--- a/sstable/colblk/column_test.go
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright 2024 The LevelDB-Go and Pebble Authors. All rights reserved. Use
-// of this source code is governed by a BSD-style license that can be found in
-// the LICENSE file.
-
-package colblk
-
-import "testing"
-
-func TestColumnDesc(t *testing.T) {
-	type testCase struct {
-		desc  ColumnDesc
-		delta DeltaEncoding
-		str   string
-	}
-
-	testCases := []testCase{
-		{
-			desc:  ColumnDesc{DataType: DataTypeBool, Encoding: EncodingDefault},
-			delta: DeltaEncodingNone,
-			str:   "bool",
-		},
-		{
-			desc:  ColumnDesc{DataType: DataTypeUint32, Encoding: EncodingDefault},
-			delta: DeltaEncodingNone,
-			str:   "uint32",
-		},
-		{
-			desc: ColumnDesc{
-				DataType: DataTypeUint32,
-				Encoding: EncodingDefault.WithDelta(DeltaEncodingUint16),
-			},
-			delta: DeltaEncodingUint16,
-			str:   "uint32+delta16",
-		},
-		{
-			desc: ColumnDesc{
-				DataType: DataTypeUint64,
-				Encoding: EncodingDefault.WithDelta(DeltaEncodingConstant),
-			},
-			delta: DeltaEncodingConstant,
-			str:   "uint64+const",
-		},
-	}
-	for _, tc := range testCases {
-		t.Run(tc.str, func(t *testing.T) {
-			if got := tc.desc.String(); got != tc.str {
-				t.Errorf("String() = %q; want %q", got, tc.str)
-			}
-			if d := tc.desc.Encoding.Delta(); d != tc.delta {
-				t.Errorf(".Delta() = %q; want %q", d, tc.delta)
-			}
-		})
-	}
-}
diff --git a/sstable/colblk/raw_bytes_test.go b/sstable/colblk/raw_bytes_test.go
index 1cf0c7b4..79732b94 100644
--- a/sstable/colblk/raw_bytes_test.go
+++ b/sstable/colblk/raw_bytes_test.go
@@ -42,7 +42,7 @@ func TestRawBytes(t *testing.T) {
 			fmt.Fprintf(&out, "Size: %d\n", size-uint32(startOffset))
 
 			buf := aligned.ByteSlice(startOffset + int(size))
-			endOffset, desc := builder.Finish(0, count, uint32(startOffset), buf)
+			endOffset := builder.Finish(0, count, uint32(startOffset), buf)
 
 			// Validate that builder.Size() was correct in its estimate.
 			require.Equal(t, size, endOffset)
@@ -50,8 +50,8 @@ func TestRawBytes(t *testing.T) {
 			if startOffset > 0 {
 				f.HexBytesln(startOffset, "start offset")
 			}
-			rawBytesToBinFormatter(f, count, desc.Encoding, nil)
-			rawBytes = MakeRawBytes(count, buf[:], 0, desc.Encoding)
+			rawBytesToBinFormatter(f, count, nil)
+			rawBytes = MakeRawBytes(count, buf[startOffset:], 0)
 			return f.String()
 		case "at":
 			var indices []int
@@ -91,7 +91,7 @@ func BenchmarkRawBytes(b *testing.B) {
 
 	var builder RawBytesBuilder
 	var buf []byte
-	buildRawBytes := func(slices [][]byte) ([]byte, ColumnEncoding) {
+	buildRawBytes := func(slices [][]byte) []byte {
 		builder.Reset()
 		for _, s := range slices {
 			builder.Put(s)
@@ -102,8 +102,8 @@ func BenchmarkRawBytes(b *testing.B) {
 		} else {
 			buf = make([]byte, sz)
 		}
-		_, desc := builder.Finish(0, len(slices), 0, buf)
-		return buf, desc.Encoding
+		_ = builder.Finish(0, len(slices), 0, buf)
+		return buf
 	}
 
 	sizes := []int{8, 128, 1024}
@@ -115,7 +115,7 @@ func BenchmarkRawBytes(b *testing.B) {
 				b.ResetTimer()
 				b.SetBytes(32 << 10)
 				for i := 0; i < b.N; i++ {
-					data, _ := buildRawBytes(slices)
+					data := buildRawBytes(slices)
 					fmt.Fprint(io.Discard, data)
 				}
 			})
@@ -125,8 +125,8 @@ func BenchmarkRawBytes(b *testing.B) {
 		for _, sz := range sizes {
 			b.Run(fmt.Sprintf("sliceLen=%d", sz), func(b *testing.B) {
 				slices := generateRandomSlices(sz, 32<<10 /* 32 KiB */)
-				data, enc := buildRawBytes(slices)
-				rb := MakeRawBytes(len(slices), data, 0, enc)
+				data := buildRawBytes(slices)
+				rb := MakeRawBytes(len(slices), data, 0)
 				b.ResetTimer()
 				b.SetBytes(32 << 10)
 				for i := 0; i < b.N; i++ {
diff --git a/sstable/colblk/testdata/block_writer b/sstable/colblk/testdata/block_writer
new file mode 100644
index 00000000..82a68832
--- /dev/null
+++ b/sstable/colblk/testdata/block_writer
@@ -0,0 +1,375 @@
+init schema=(uint64)
+----
+
+write
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+----
+
+finish
+----
+# columnar block header
+00-01: x 01               # version 1
+01-03: x 0100             # 1 columns
+03-07: x 0a000000         # 10 rows
+# column 0
+07-08: b 00000101         # uint64
+08-12: x 0c000000         # page start 12
+# data for column 0
+12-13: x 01               # delta encoding: const
+13-21: x 0000000000000000 # 64-bit constant: 0
+21-22: x 00               # block trailer padding
+
+# Test a uint64 column with all values equal but non-zero.
+
+init schema=(uint64)
+----
+
+write
+9223372036854775807
+9223372036854775807
+9223372036854775807
+9223372036854775807
+9223372036854775807
+9223372036854775807
+9223372036854775807
+9223372036854775807
+9223372036854775807
+9223372036854775807
+9223372036854775807
+9223372036854775807
+----
+
+finish
+----
+# columnar block header
+00-01: x 01               # version 1
+01-03: x 0100             # 1 columns
+03-07: x 0c000000         # 12 rows
+# column 0
+07-08: b 00000101         # uint64
+08-12: x 0c000000         # page start 12
+# data for column 0
+12-13: x 01               # delta encoding: const
+13-21: x ffffffffffffff7f # 64-bit constant: 9223372036854775807
+21-22: x 00               # block trailer padding
+
+# Test a uint64 column with a mix of values, but all values less than 256
+# greater than 4149660732785475243. It should use the delta8 encoding.
+
+init schema=(uint64)
+----
+
+write
+4149660732785475244
+4149660732785475243
+4149660732785475294
+4149660732785475324
+4149660732785475390
+----
+
+finish
+----
+# columnar block header
+00-01: x 01               # version 1
+01-03: x 0100             # 1 columns
+03-07: x 05000000         # 5 rows
+# column 0
+07-08: b 00000101         # uint64
+08-12: x 0c000000         # page start 12
+# data for column 0
+12-13: x 02               # delta encoding: delta8
+13-21: x abbe105c738e9639 # 64-bit constant: 4149660732785475243
+21-22: x 01               # data[0] = 1
+22-23: x 00               # data[1] = 0
+23-24: x 33               # data[2] = 51
+24-25: x 51               # data[3] = 81
+25-26: x 93               # data[4] = 147
+26-27: x 00               # block trailer padding
+
+# Test the same case, but this time with a value that is exactly 256 greater
+# than the lowest value. The column should use the delta16 encoding.
+
+init schema=(uint64)
+----
+
+write
+4149660732785475244
+4149660732785475243
+4149660732785475294
+4149660732785475499
+4149660732785475324
+4149660732785475390
+----
+
+finish
+----
+# columnar block header
+00-01: x 01               # version 1
+01-03: x 0100             # 1 columns
+03-07: x 06000000         # 6 rows
+# column 0
+07-08: b 00000101         # uint64
+08-12: x 0c000000         # page start 12
+# data for column 0
+12-13: x 03               # delta encoding: delta16
+13-21: x abbe105c738e9639 # 64-bit constant: 4149660732785475243
+# padding
+21-22: x 00               # aligning to 16-bit boundary
+22-24: x 0100             # data[0] = 1
+24-26: x 0000             # data[1] = 0
+26-28: x 3300             # data[2] = 51
+28-30: x 0001             # data[3] = 256
+30-32: x 5100             # data[4] = 81
+32-34: x 9300             # data[5] = 147
+34-35: x 00               # block trailer padding
+
+init schema=(uint64)
+----
+
+write
+0
+1
+2
+3
+4
+5
+6
+2147483647
+65536
+----
+
+finish
+----
+# columnar block header
+00-01: x 01               # version 1
+01-03: x 0100             # 1 columns
+03-07: x 09000000         # 9 rows
+# column 0
+07-08: b 00000101         # uint64
+08-12: x 0c000000         # page start 12
+# data for column 0
+12-13: x 04               # delta encoding: delta32
+13-21: x 0000000000000000 # 64-bit constant: 0
+# padding
+21-24: x 000000           # aligning to 32-bit boundary
+24-28: x 00000000         # data[0] = 0
+28-32: x 01000000         # data[1] = 1
+32-36: x 02000000         # data[2] = 2
+36-40: x 03000000         # data[3] = 3
+40-44: x 04000000         # data[4] = 4
+44-48: x 05000000         # data[5] = 5
+48-52: x 06000000         # data[6] = 6
+52-56: x ffffff7f         # data[7] = 2147483647
+56-60: x 00000100         # data[8] = 65536
+60-61: x 00               # block trailer padding
+
+# Test two columns: a uint32 and a uint64.
+
+init schema=(uint32,uint64)
+----
+
+write
+0 0
+1 1
+2 2
+3 3
+4 4
+5 5
+6 6
+7 7
+8 8
+9 9
+----
+
+finish
+----
+# columnar block header
+00-01: x 01               # version 1
+01-03: x 0200             # 2 columns
+03-07: x 0a000000         # 10 rows
+# column 0
+07-08: b 00000100         # uint32
+08-12: x 11000000         # page start 17
+# column 1
+12-13: b 00000101         # uint64
+13-17: x 20000000         # page start 32
+# data for column 0
+17-18: x 02               # delta encoding: delta8
+18-22: x 00000000         # 32-bit constant: 0
+22-23: x 00               # data[0] = 0
+23-24: x 01               # data[1] = 1
+24-25: x 02               # data[2] = 2
+25-26: x 03               # data[3] = 3
+26-27: x 04               # data[4] = 4
+27-28: x 05               # data[5] = 5
+28-29: x 06               # data[6] = 6
+29-30: x 07               # data[7] = 7
+30-31: x 08               # data[8] = 8
+31-32: x 09               # data[9] = 9
+# data for column 1
+32-33: x 02               # delta encoding: delta8
+33-41: x 0000000000000000 # 64-bit constant: 0
+41-42: x 00               # data[0] = 0
+42-43: x 01               # data[1] = 1
+43-44: x 02               # data[2] = 2
+44-45: x 03               # data[3] = 3
+45-46: x 04               # data[4] = 4
+46-47: x 05               # data[5] = 5
+47-48: x 06               # data[6] = 6
+48-49: x 07               # data[7] = 7
+49-50: x 08               # data[8] = 8
+50-51: x 09               # data[9] = 9
+51-52: x 00               # block trailer padding
+
+init schema=(uint32,uint64)
+----
+
+write
+12295 2592525825202
+1 1
+----
+
+finish
+----
+# columnar block header
+00-01: x 01               # version 1
+01-03: x 0200             # 2 columns
+03-07: x 02000000         # 2 rows
+# column 0
+07-08: b 00000100         # uint32
+08-12: x 11000000         # page start 17
+# column 1
+12-13: b 00000101         # uint64
+13-17: x 1a000000         # page start 26
+# data for column 0
+17-18: x 03               # delta encoding: delta16
+18-22: x 01000000         # 32-bit constant: 1
+22-24: x 0630             # data[0] = 12294
+24-26: x 0000             # data[1] = 0
+# data for column 1
+26-27: x 00               # delta encoding: none
+# padding
+27-32: x 0000000000       # aligning to 64-bit boundary
+32-40: x b2b4949e5b020000 # data[0] = 2592525825202
+40-48: x 0100000000000000 # data[1] = 1
+48-49: x 00               # block trailer padding
+
+init schema=(bool)
+----
+
+write
+t
+t
+t
+f
+t
+t
+f
+t
+t
+f
+t
+t
+t
+t
+t
+f
+t
+t
+f
+t
+t
+f
+t
+t
+----
+
+finish
+----
+# columnar block header
+00-01: x 01                                                               # version 1
+01-03: x 0100                                                             # 1 columns
+03-07: x 18000000                                                         # 24 rows
+# column 0
+07-08: b 00000001                                                         # bool
+08-12: x 0c000000                                                         # page start 12
+# data for column 0
+12-16: x 00000000                                                         # padding to align to 64-bit boundary
+16-24: b 1011011101111101110110110000000000000000000000000000000000000000 # bitmap word 0
+24-32: b 0000000100000000000000000000000000000000000000000000000000000000 # bitmap summary word 0-63
+32-33: x 00                                                               # block trailer padding
+
+init schema=(bytes,uint64)
+----
+
+write
+apple 20
+banana 30
+coconut 10
+dragonfruit 70
+elderberry 50
+fraise 60
+goji 80
+jackfruit 90
+kiwi 100
+----
+
+finish
+----
+# columnar block header
+000-001: x 01                     # version 1
+001-003: x 0200                   # 2 columns
+003-007: x 09000000               # 9 rows
+# column 0
+007-008: b 00000110               # bytes
+008-012: x 11000000               # page start 17
+# column 1
+012-013: b 00000101               # uint64
+013-017: x 5e000000               # page start 94
+# data for column 0
+# rawbytes
+# offsets table
+017-018: x 02                     # delta encoding: delta8
+018-022: x 00000000               # 32-bit constant: 0
+022-023: x 00                     # data[0] = 0 [32 overall]
+023-024: x 05                     # data[1] = 5 [37 overall]
+024-025: x 0b                     # data[2] = 11 [43 overall]
+025-026: x 12                     # data[3] = 18 [50 overall]
+026-027: x 1d                     # data[4] = 29 [61 overall]
+027-028: x 27                     # data[5] = 39 [71 overall]
+028-029: x 2d                     # data[6] = 45 [77 overall]
+029-030: x 31                     # data[7] = 49 [81 overall]
+030-031: x 3a                     # data[8] = 58 [90 overall]
+031-032: x 3e                     # data[9] = 62 [94 overall]
+# data
+032-037: x 6170706c65             # data[0]: apple
+037-043: x 62616e616e61           # data[1]: banana
+043-050: x 636f636f6e7574         # data[2]: coconut
+050-061: x 647261676f6e6672756974 # data[3]: dragonfruit
+061-071: x 656c6465726265727279   # data[4]: elderberry
+071-077: x 667261697365           # data[5]: fraise
+077-081: x 676f6a69               # data[6]: goji
+081-090: x 6a61636b6672756974     # data[7]: jackfruit
+090-094: x 6b697769               # data[8]: kiwi
+# data for column 1
+094-095: x 02                     # delta encoding: delta8
+095-103: x 0a00000000000000       # 64-bit constant: 10
+103-104: x 0a                     # data[0] = 10
+104-105: x 14                     # data[1] = 20
+105-106: x 00                     # data[2] = 0
+106-107: x 3c                     # data[3] = 60
+107-108: x 28                     # data[4] = 40
+108-109: x 32                     # data[5] = 50
+109-110: x 46                     # data[6] = 70
+110-111: x 50                     # data[7] = 80
+111-112: x 5a                     # data[8] = 90
+112-113: x 00                     # block trailer padding
diff --git a/sstable/colblk/testdata/raw_bytes b/sstable/colblk/testdata/raw_bytes
index 4b59abc3..9ae4cfa9 100644
--- a/sstable/colblk/testdata/raw_bytes
+++ b/sstable/colblk/testdata/raw_bytes
@@ -3,13 +3,14 @@
 build offset=0
 a
 ----
-# RawBytes
-# Offsets table
-0-4: x 00000000 # 32-bit constant: 0
-4-5: x 00       # data[0] = 0 [6 overall]
-5-6: x 01       # data[1] = 1 [7 overall]
-# Data
-6-7: x 61       # data[0]: a
+# rawbytes
+# offsets table
+0-1: x 02       # delta encoding: delta8
+1-5: x 00000000 # 32-bit constant: 0
+5-6: x 00       # data[0] = 0 [7 overall]
+6-7: x 01       # data[1] = 1 [8 overall]
+# data
+7-8: x 61       # data[0]: a
 
 # Try the same thing, but with offsets 1, 2, 3, 4.
 
@@ -17,110 +18,118 @@ build offset=1
 a
 ----
 0-1: x 00       # start offset
-# RawBytes
-# Offsets table
-1-5: x 00000000 # 32-bit constant: 0
-5-6: x 00       # data[0] = 0 [7 overall]
-6-7: x 01       # data[1] = 1 [8 overall]
-# Data
-7-8: x 61       # data[0]: a
+# rawbytes
+# offsets table
+1-2: x 02       # delta encoding: delta8
+2-6: x 00000000 # 32-bit constant: 0
+6-7: x 00       # data[0] = 0 [8 overall]
+7-8: x 01       # data[1] = 1 [9 overall]
+# data
+8-9: x 61       # data[0]: a
 
 build offset=2
 a
 ----
 00-02: x 0000     # start offset
-# RawBytes
-# Offsets table
-02-06: x 00000000 # 32-bit constant: 0
-06-07: x 00       # data[0] = 0 [8 overall]
-07-08: x 01       # data[1] = 1 [9 overall]
-# Data
-08-09: x 61       # data[0]: a
-
-build offset=3
-a
-----
-00-03: x 000000   # start offset
-# RawBytes
-# Offsets table
+# rawbytes
+# offsets table
+02-03: x 02       # delta encoding: delta8
 03-07: x 00000000 # 32-bit constant: 0
 07-08: x 00       # data[0] = 0 [9 overall]
 08-09: x 01       # data[1] = 1 [10 overall]
-# Data
+# data
 09-10: x 61       # data[0]: a
 
-build offset=4
+build offset=3
 a
 ----
-00-04: x 00000000 # start offset
-# RawBytes
-# Offsets table
+00-03: x 000000   # start offset
+# rawbytes
+# offsets table
+03-04: x 02       # delta encoding: delta8
 04-08: x 00000000 # 32-bit constant: 0
 08-09: x 00       # data[0] = 0 [10 overall]
 09-10: x 01       # data[1] = 1 [11 overall]
-# Data
+# data
 10-11: x 61       # data[0]: a
 
+build offset=4
+a
+----
+00-04: x 00000000 # start offset
+# rawbytes
+# offsets table
+04-05: x 02       # delta encoding: delta8
+05-09: x 00000000 # 32-bit constant: 0
+09-10: x 00       # data[0] = 0 [11 overall]
+10-11: x 01       # data[1] = 1 [12 overall]
+# data
+11-12: x 61       # data[0]: a
+
 # Create a RawBytes with two byte slices: 'a' and 'b'.
 
 build offset=0
 a
 b
 ----
-# RawBytes
-# Offsets table
-0-4: x 00000000 # 32-bit constant: 0
-4-5: x 00       # data[0] = 0 [7 overall]
-5-6: x 01       # data[1] = 1 [8 overall]
-6-7: x 02       # data[2] = 2 [9 overall]
-# Data
-7-8: x 61       # data[0]: a
-8-9: x 62       # data[1]: b
+# rawbytes
+# offsets table
+0-1: x 02       # delta encoding: delta8
+1-5: x 00000000 # 32-bit constant: 0
+5-6: x 00       # data[0] = 0 [8 overall]
+6-7: x 01       # data[1] = 1 [9 overall]
+7-8: x 02       # data[2] = 2 [10 overall]
+# data
+8-9: x 61       # data[0]: a
+9-10: x 62      # data[1]: b
 
 build offset=0
 a
 ab
 abc
 ----
-# RawBytes
-# Offsets table
-00-04: x 00000000 # 32-bit constant: 0
-04-05: x 00       # data[0] = 0 [8 overall]
-05-06: x 01       # data[1] = 1 [9 overall]
-06-07: x 03       # data[2] = 3 [11 overall]
-07-08: x 06       # data[3] = 6 [14 overall]
-# Data
-08-09: x 61       # data[0]: a
-09-11: x 6162     # data[1]: ab
-11-14: x 616263   # data[2]: abc
+# rawbytes
+# offsets table
+00-01: x 02       # delta encoding: delta8
+01-05: x 00000000 # 32-bit constant: 0
+05-06: x 00       # data[0] = 0 [9 overall]
+06-07: x 01       # data[1] = 1 [10 overall]
+07-08: x 03       # data[2] = 3 [12 overall]
+08-09: x 06       # data[3] = 6 [15 overall]
+# data
+09-10: x 61       # data[0]: a
+10-12: x 6162     # data[1]: ab
+12-15: x 616263   # data[2]: abc
 
 build offset=0
 aaabbbc
 ----
-# RawBytes
-# Offsets table
-00-04: x 00000000       # 32-bit constant: 0
-04-05: x 00             # data[0] = 0 [6 overall]
-05-06: x 07             # data[1] = 7 [13 overall]
-# Data
-06-13: x 61616162626263 # data[0]: aaabbbc
+# rawbytes
+# offsets table
+00-01: x 02             # delta encoding: delta8
+01-05: x 00000000       # 32-bit constant: 0
+05-06: x 00             # data[0] = 0 [7 overall]
+06-07: x 07             # data[1] = 7 [14 overall]
+# data
+07-14: x 61616162626263 # data[0]: aaabbbc
 
 build offset=0
 a
 ab
 abc
 ----
-# RawBytes
-# Offsets table
-00-04: x 00000000 # 32-bit constant: 0
-04-05: x 00       # data[0] = 0 [8 overall]
-05-06: x 01       # data[1] = 1 [9 overall]
-06-07: x 03       # data[2] = 3 [11 overall]
-07-08: x 06       # data[3] = 6 [14 overall]
-# Data
-08-09: x 61       # data[0]: a
-09-11: x 6162     # data[1]: ab
-11-14: x 616263   # data[2]: abc
+# rawbytes
+# offsets table
+00-01: x 02       # delta encoding: delta8
+01-05: x 00000000 # 32-bit constant: 0
+05-06: x 00       # data[0] = 0 [9 overall]
+06-07: x 01       # data[1] = 1 [10 overall]
+07-08: x 03       # data[2] = 3 [12 overall]
+08-09: x 06       # data[3] = 6 [15 overall]
+# data
+09-10: x 61       # data[0]: a
+10-12: x 6162     # data[1]: ab
+12-15: x 616263   # data[2]: abc
 
 at indices=(0, 1, 2)
 ----
@@ -139,29 +148,30 @@ cantelope
 lettuce
 kale
 ----
-# RawBytes
-# Offsets table
-00-04: x 00000000           # 32-bit constant: 0
-04-05: x 00                 # data[0] = 0 [14 overall]
-05-06: x 03                 # data[1] = 3 [17 overall]
-06-07: x 09                 # data[2] = 9 [23 overall]
-07-08: x 11                 # data[3] = 17 [31 overall]
-08-09: x 16                 # data[4] = 22 [36 overall]
-09-10: x 1b                 # data[5] = 27 [41 overall]
-10-11: x 21                 # data[6] = 33 [47 overall]
-11-12: x 2a                 # data[7] = 42 [56 overall]
-12-13: x 31                 # data[8] = 49 [63 overall]
-13-14: x 35                 # data[9] = 53 [67 overall]
-# Data
-14-17: x 636174             # data[0]: cat
-17-23: x 6f72616e6765       # data[1]: orange
-23-31: x 7a75636368696e69   # data[2]: zucchini
-31-36: x 6c656d6f6e         # data[3]: lemon
-36-41: x 6170706c65         # data[4]: apple
-41-47: x 62616e616e61       # data[5]: banana
-47-56: x 63616e74656c6f7065 # data[6]: cantelope
-56-63: x 6c657474756365     # data[7]: lettuce
-63-67: x 6b616c65           # data[8]: kale
+# rawbytes
+# offsets table
+00-01: x 02                 # delta encoding: delta8
+01-05: x 00000000           # 32-bit constant: 0
+05-06: x 00                 # data[0] = 0 [15 overall]
+06-07: x 03                 # data[1] = 3 [18 overall]
+07-08: x 09                 # data[2] = 9 [24 overall]
+08-09: x 11                 # data[3] = 17 [32 overall]
+09-10: x 16                 # data[4] = 22 [37 overall]
+10-11: x 1b                 # data[5] = 27 [42 overall]
+11-12: x 21                 # data[6] = 33 [48 overall]
+12-13: x 2a                 # data[7] = 42 [57 overall]
+13-14: x 31                 # data[8] = 49 [64 overall]
+14-15: x 35                 # data[9] = 53 [68 overall]
+# data
+15-18: x 636174             # data[0]: cat
+18-24: x 6f72616e6765       # data[1]: orange
+24-32: x 7a75636368696e69   # data[2]: zucchini
+32-37: x 6c656d6f6e         # data[3]: lemon
+37-42: x 6170706c65         # data[4]: apple
+42-48: x 62616e616e61       # data[5]: banana
+48-57: x 63616e74656c6f7065 # data[6]: cantelope
+57-64: x 6c657474756365     # data[7]: lettuce
+64-68: x 6b616c65           # data[8]: kale
 
 at indices=(0, 1, 2, 3, 4, 5, 6, 7, 8)
 ----
@@ -188,19 +198,20 @@ cantelope
 lettuce
 kale
 ----
-# RawBytes
-# Offsets table
-00-04: x 00000000         # 32-bit constant: 0
-04-05: x 00               # data[0] = 0 [9 overall]
-05-06: x 03               # data[1] = 3 [12 overall]
-06-07: x 09               # data[2] = 9 [18 overall]
-07-08: x 11               # data[3] = 17 [26 overall]
-08-09: x 16               # data[4] = 22 [31 overall]
-# Data
-09-12: x 636174           # data[0]: cat
-12-18: x 6f72616e6765     # data[1]: orange
-18-26: x 7a75636368696e69 # data[2]: zucchini
-26-31: x 6c656d6f6e       # data[3]: lemon
+# rawbytes
+# offsets table
+00-01: x 02               # delta encoding: delta8
+01-05: x 00000000         # 32-bit constant: 0
+05-06: x 00               # data[0] = 0 [10 overall]
+06-07: x 03               # data[1] = 3 [13 overall]
+07-08: x 09               # data[2] = 9 [19 overall]
+08-09: x 11               # data[3] = 17 [27 overall]
+09-10: x 16               # data[4] = 22 [32 overall]
+# data
+10-13: x 636174           # data[0]: cat
+13-19: x 6f72616e6765     # data[1]: orange
+19-27: x 7a75636368696e69 # data[2]: zucchini
+27-32: x 6c656d6f6e       # data[3]: lemon
 
 at indices=(0, 1, 2, 3)
 ----
diff --git a/sstable/colblk/testdata/uints b/sstable/colblk/testdata/uints
index 49922f51..7cb824e3 100644
--- a/sstable/colblk/testdata/uints
+++ b/sstable/colblk/testdata/uints
@@ -25,32 +25,32 @@ write
 size rows=(5, 4, 3, 2, 1, 0)
 ----
 b8:
-  8: *colblk.UintBuilder[uint8].Size(5, 0) = 1
-  8: *colblk.UintBuilder[uint8].Size(4, 0) = 1
-  8: *colblk.UintBuilder[uint8].Size(3, 0) = 1
-  8: *colblk.UintBuilder[uint8].Size(2, 0) = 1
-  8: *colblk.UintBuilder[uint8].Size(1, 0) = 1
+  8: *colblk.UintBuilder[uint8].Size(5, 0) = 2
+  8: *colblk.UintBuilder[uint8].Size(4, 0) = 2
+  8: *colblk.UintBuilder[uint8].Size(3, 0) = 2
+  8: *colblk.UintBuilder[uint8].Size(2, 0) = 2
+  8: *colblk.UintBuilder[uint8].Size(1, 0) = 2
   8: *colblk.UintBuilder[uint8].Size(0, 0) = 0
 b16:
-  16: *colblk.UintBuilder[uint16].Size(5, 0) = 2
-  16: *colblk.UintBuilder[uint16].Size(4, 0) = 2
-  16: *colblk.UintBuilder[uint16].Size(3, 0) = 2
-  16: *colblk.UintBuilder[uint16].Size(2, 0) = 2
-  16: *colblk.UintBuilder[uint16].Size(1, 0) = 2
+  16: *colblk.UintBuilder[uint16].Size(5, 0) = 3
+  16: *colblk.UintBuilder[uint16].Size(4, 0) = 3
+  16: *colblk.UintBuilder[uint16].Size(3, 0) = 3
+  16: *colblk.UintBuilder[uint16].Size(2, 0) = 3
+  16: *colblk.UintBuilder[uint16].Size(1, 0) = 3
   16: *colblk.UintBuilder[uint16].Size(0, 0) = 0
 b32:
-  32: *colblk.UintBuilder[uint32].Size(5, 0) = 4
-  32: *colblk.UintBuilder[uint32].Size(4, 0) = 4
-  32: *colblk.UintBuilder[uint32].Size(3, 0) = 4
-  32: *colblk.UintBuilder[uint32].Size(2, 0) = 4
-  32: *colblk.UintBuilder[uint32].Size(1, 0) = 4
+  32: *colblk.UintBuilder[uint32].Size(5, 0) = 5
+  32: *colblk.UintBuilder[uint32].Size(4, 0) = 5
+  32: *colblk.UintBuilder[uint32].Size(3, 0) = 5
+  32: *colblk.UintBuilder[uint32].Size(2, 0) = 5
+  32: *colblk.UintBuilder[uint32].Size(1, 0) = 5
   32: *colblk.UintBuilder[uint32].Size(0, 0) = 0
 b64:
-  64: *colblk.UintBuilder[uint64].Size(5, 0) = 8
-  64: *colblk.UintBuilder[uint64].Size(4, 0) = 8
-  64: *colblk.UintBuilder[uint64].Size(3, 0) = 8
-  64: *colblk.UintBuilder[uint64].Size(2, 0) = 8
-  64: *colblk.UintBuilder[uint64].Size(1, 0) = 8
+  64: *colblk.UintBuilder[uint64].Size(5, 0) = 9
+  64: *colblk.UintBuilder[uint64].Size(4, 0) = 9
+  64: *colblk.UintBuilder[uint64].Size(3, 0) = 9
+  64: *colblk.UintBuilder[uint64].Size(2, 0) = 9
+  64: *colblk.UintBuilder[uint64].Size(1, 0) = 9
   64: *colblk.UintBuilder[uint64].Size(0, 0) = 0
 
 # Add a nonzero value. Size calls that include the new row count should
@@ -66,58 +66,59 @@ write
 size rows=(8, 7, 6, 5, 4, 3, 2, 1, 0)
 ----
 b8:
-  8: *colblk.UintBuilder[uint8].Size(8, 0) = 8
-  8: *colblk.UintBuilder[uint8].Size(7, 0) = 7
-  8: *colblk.UintBuilder[uint8].Size(6, 0) = 6
-  8: *colblk.UintBuilder[uint8].Size(5, 0) = 1
-  8: *colblk.UintBuilder[uint8].Size(4, 0) = 1
-  8: *colblk.UintBuilder[uint8].Size(3, 0) = 1
-  8: *colblk.UintBuilder[uint8].Size(2, 0) = 1
-  8: *colblk.UintBuilder[uint8].Size(1, 0) = 1
+  8: *colblk.UintBuilder[uint8].Size(8, 0) = 9
+  8: *colblk.UintBuilder[uint8].Size(7, 0) = 8
+  8: *colblk.UintBuilder[uint8].Size(6, 0) = 7
+  8: *colblk.UintBuilder[uint8].Size(5, 0) = 2
+  8: *colblk.UintBuilder[uint8].Size(4, 0) = 2
+  8: *colblk.UintBuilder[uint8].Size(3, 0) = 2
+  8: *colblk.UintBuilder[uint8].Size(2, 0) = 2
+  8: *colblk.UintBuilder[uint8].Size(1, 0) = 2
   8: *colblk.UintBuilder[uint8].Size(0, 0) = 0
 b16:
-  16: *colblk.UintBuilder[uint16].Size(8, 0) = 10
-  16: *colblk.UintBuilder[uint16].Size(7, 0) = 9
-  16: *colblk.UintBuilder[uint16].Size(6, 0) = 8
-  16: *colblk.UintBuilder[uint16].Size(5, 0) = 2
-  16: *colblk.UintBuilder[uint16].Size(4, 0) = 2
-  16: *colblk.UintBuilder[uint16].Size(3, 0) = 2
-  16: *colblk.UintBuilder[uint16].Size(2, 0) = 2
-  16: *colblk.UintBuilder[uint16].Size(1, 0) = 2
+  16: *colblk.UintBuilder[uint16].Size(8, 0) = 11
+  16: *colblk.UintBuilder[uint16].Size(7, 0) = 10
+  16: *colblk.UintBuilder[uint16].Size(6, 0) = 9
+  16: *colblk.UintBuilder[uint16].Size(5, 0) = 3
+  16: *colblk.UintBuilder[uint16].Size(4, 0) = 3
+  16: *colblk.UintBuilder[uint16].Size(3, 0) = 3
+  16: *colblk.UintBuilder[uint16].Size(2, 0) = 3
+  16: *colblk.UintBuilder[uint16].Size(1, 0) = 3
   16: *colblk.UintBuilder[uint16].Size(0, 0) = 0
 b32:
-  32: *colblk.UintBuilder[uint32].Size(8, 0) = 12
-  32: *colblk.UintBuilder[uint32].Size(7, 0) = 11
-  32: *colblk.UintBuilder[uint32].Size(6, 0) = 10
-  32: *colblk.UintBuilder[uint32].Size(5, 0) = 4
-  32: *colblk.UintBuilder[uint32].Size(4, 0) = 4
-  32: *colblk.UintBuilder[uint32].Size(3, 0) = 4
-  32: *colblk.UintBuilder[uint32].Size(2, 0) = 4
-  32: *colblk.UintBuilder[uint32].Size(1, 0) = 4
+  32: *colblk.UintBuilder[uint32].Size(8, 0) = 13
+  32: *colblk.UintBuilder[uint32].Size(7, 0) = 12
+  32: *colblk.UintBuilder[uint32].Size(6, 0) = 11
+  32: *colblk.UintBuilder[uint32].Size(5, 0) = 5
+  32: *colblk.UintBuilder[uint32].Size(4, 0) = 5
+  32: *colblk.UintBuilder[uint32].Size(3, 0) = 5
+  32: *colblk.UintBuilder[uint32].Size(2, 0) = 5
+  32: *colblk.UintBuilder[uint32].Size(1, 0) = 5
   32: *colblk.UintBuilder[uint32].Size(0, 0) = 0
 b64:
-  64: *colblk.UintBuilder[uint64].Size(8, 0) = 16
-  64: *colblk.UintBuilder[uint64].Size(7, 0) = 15
-  64: *colblk.UintBuilder[uint64].Size(6, 0) = 14
-  64: *colblk.UintBuilder[uint64].Size(5, 0) = 8
-  64: *colblk.UintBuilder[uint64].Size(4, 0) = 8
-  64: *colblk.UintBuilder[uint64].Size(3, 0) = 8
-  64: *colblk.UintBuilder[uint64].Size(2, 0) = 8
-  64: *colblk.UintBuilder[uint64].Size(1, 0) = 8
+  64: *colblk.UintBuilder[uint64].Size(8, 0) = 17
+  64: *colblk.UintBuilder[uint64].Size(7, 0) = 16
+  64: *colblk.UintBuilder[uint64].Size(6, 0) = 15
+  64: *colblk.UintBuilder[uint64].Size(5, 0) = 9
+  64: *colblk.UintBuilder[uint64].Size(4, 0) = 9
+  64: *colblk.UintBuilder[uint64].Size(3, 0) = 9
+  64: *colblk.UintBuilder[uint64].Size(2, 0) = 9
+  64: *colblk.UintBuilder[uint64].Size(1, 0) = 9
   64: *colblk.UintBuilder[uint64].Size(0, 0) = 0
 
 # Finish the b8 so we can test 16-bit encoding.
 finish widths=(8) rows=8
 ----
 b8: *colblk.UintBuilder[uint8]:
-0-1: x 00 # data[0] = 0
-1-2: x 00 # data[1] = 0
-2-3: x 00 # data[2] = 0
-3-4: x 00 # data[3] = 0
-4-5: x 00 # data[4] = 0
-5-6: x 0a # data[5] = 10
-6-7: x 00 # data[6] = 0
-7-8: x 0a # data[7] = 10
+0-1: x 00 # delta encoding: none
+1-2: x 00 # data[0] = 0
+2-3: x 00 # data[1] = 0
+3-4: x 00 # data[2] = 0
+4-5: x 00 # data[3] = 0
+5-6: x 00 # data[4] = 0
+6-7: x 0a # data[5] = 10
+7-8: x 00 # data[6] = 0
+8-9: x 0a # data[7] = 10
 Keeping b16 open
 Keeping b32 open
 Keeping b64 open
@@ -131,51 +132,51 @@ write
 size rows=(9, 8)
 ----
 b16:
-  16: *colblk.UintBuilder[uint16].Size(9, 0) = 18
-  16: *colblk.UintBuilder[uint16].Size(8, 0) = 10
+  16: *colblk.UintBuilder[uint16].Size(9, 0) = 20
+  16: *colblk.UintBuilder[uint16].Size(8, 0) = 11
 b32:
-  32: *colblk.UintBuilder[uint32].Size(9, 0) = 22
-  32: *colblk.UintBuilder[uint32].Size(8, 0) = 12
+  32: *colblk.UintBuilder[uint32].Size(9, 0) = 24
+  32: *colblk.UintBuilder[uint32].Size(8, 0) = 13
 b64:
-  64: *colblk.UintBuilder[uint64].Size(9, 0) = 26
-  64: *colblk.UintBuilder[uint64].Size(8, 0) = 16
+  64: *colblk.UintBuilder[uint64].Size(9, 0) = 28
+  64: *colblk.UintBuilder[uint64].Size(8, 0) = 17
 
 size rows=(9, 8) offset=1
 ----
 b16:
   16: *colblk.UintBuilder[uint16].Size(9, 1) = 20 [19 w/o offset]
-  16: *colblk.UintBuilder[uint16].Size(8, 1) = 11 [10 w/o offset]
+  16: *colblk.UintBuilder[uint16].Size(8, 1) = 12 [11 w/o offset]
 b32:
   32: *colblk.UintBuilder[uint32].Size(9, 1) = 24 [23 w/o offset]
-  32: *colblk.UintBuilder[uint32].Size(8, 1) = 13 [12 w/o offset]
+  32: *colblk.UintBuilder[uint32].Size(8, 1) = 14 [13 w/o offset]
 b64:
   64: *colblk.UintBuilder[uint64].Size(9, 1) = 28 [27 w/o offset]
-  64: *colblk.UintBuilder[uint64].Size(8, 1) = 17 [16 w/o offset]
+  64: *colblk.UintBuilder[uint64].Size(8, 1) = 18 [17 w/o offset]
 
 size rows=(9, 8) offset=2
 ----
 b16:
-  16: *colblk.UintBuilder[uint16].Size(9, 2) = 20 [18 w/o offset]
-  16: *colblk.UintBuilder[uint16].Size(8, 2) = 12 [10 w/o offset]
+  16: *colblk.UintBuilder[uint16].Size(9, 2) = 22 [20 w/o offset]
+  16: *colblk.UintBuilder[uint16].Size(8, 2) = 13 [11 w/o offset]
 b32:
-  32: *colblk.UintBuilder[uint32].Size(9, 2) = 24 [22 w/o offset]
-  32: *colblk.UintBuilder[uint32].Size(8, 2) = 14 [12 w/o offset]
+  32: *colblk.UintBuilder[uint32].Size(9, 2) = 26 [24 w/o offset]
+  32: *colblk.UintBuilder[uint32].Size(8, 2) = 15 [13 w/o offset]
 b64:
-  64: *colblk.UintBuilder[uint64].Size(9, 2) = 28 [26 w/o offset]
-  64: *colblk.UintBuilder[uint64].Size(8, 2) = 18 [16 w/o offset]
+  64: *colblk.UintBuilder[uint64].Size(9, 2) = 30 [28 w/o offset]
+  64: *colblk.UintBuilder[uint64].Size(8, 2) = 19 [17 w/o offset]
 
 
 size rows=(9, 8) offset=5
 ----
 b16:
   16: *colblk.UintBuilder[uint16].Size(9, 5) = 24 [19 w/o offset]
-  16: *colblk.UintBuilder[uint16].Size(8, 5) = 15 [10 w/o offset]
+  16: *colblk.UintBuilder[uint16].Size(8, 5) = 16 [11 w/o offset]
 b32:
   32: *colblk.UintBuilder[uint32].Size(9, 5) = 28 [23 w/o offset]
-  32: *colblk.UintBuilder[uint32].Size(8, 5) = 17 [12 w/o offset]
+  32: *colblk.UintBuilder[uint32].Size(8, 5) = 18 [13 w/o offset]
 b64:
   64: *colblk.UintBuilder[uint64].Size(9, 5) = 32 [27 w/o offset]
-  64: *colblk.UintBuilder[uint64].Size(8, 5) = 21 [16 w/o offset]
+  64: *colblk.UintBuilder[uint64].Size(8, 5) = 22 [17 w/o offset]
 
 # We should be able to write up to 2^16-1 without triggering a 32-bit encoding.
 
@@ -186,17 +187,17 @@ write
 size rows=(10, 9, 8)
 ----
 b16:
-  16: *colblk.UintBuilder[uint16].Size(10, 0) = 20
-  16: *colblk.UintBuilder[uint16].Size(9, 0) = 18
-  16: *colblk.UintBuilder[uint16].Size(8, 0) = 10
+  16: *colblk.UintBuilder[uint16].Size(10, 0) = 22
+  16: *colblk.UintBuilder[uint16].Size(9, 0) = 20
+  16: *colblk.UintBuilder[uint16].Size(8, 0) = 11
 b32:
-  32: *colblk.UintBuilder[uint32].Size(10, 0) = 24
-  32: *colblk.UintBuilder[uint32].Size(9, 0) = 22
-  32: *colblk.UintBuilder[uint32].Size(8, 0) = 12
+  32: *colblk.UintBuilder[uint32].Size(10, 0) = 26
+  32: *colblk.UintBuilder[uint32].Size(9, 0) = 24
+  32: *colblk.UintBuilder[uint32].Size(8, 0) = 13
 b64:
-  64: *colblk.UintBuilder[uint64].Size(10, 0) = 28
-  64: *colblk.UintBuilder[uint64].Size(9, 0) = 26
-  64: *colblk.UintBuilder[uint64].Size(8, 0) = 16
+  64: *colblk.UintBuilder[uint64].Size(10, 0) = 30
+  64: *colblk.UintBuilder[uint64].Size(9, 0) = 28
+  64: *colblk.UintBuilder[uint64].Size(8, 0) = 17
 
 # But 2^16 should trigger a 32-bit encoding. (Finish b16 so we can test 32-bit
 # encoding.)
@@ -204,16 +205,19 @@ b64:
 finish widths=(16) rows=10
 ----
 b16: *colblk.UintBuilder[uint16]:
-00-02: x 0000 # data[0] = 0
-02-04: x 0000 # data[1] = 0
-04-06: x 0000 # data[2] = 0
-06-08: x 0000 # data[3] = 0
-08-10: x 0000 # data[4] = 0
-10-12: x 0a00 # data[5] = 10
-12-14: x 0000 # data[6] = 0
-14-16: x 0a00 # data[7] = 10
-16-18: x e803 # data[8] = 1000
-18-20: x ffff # data[9] = 65535
+00-01: x 00   # delta encoding: none
+# padding
+01-02: x 00   # aligning to 16-bit boundary
+02-04: x 0000 # data[0] = 0
+04-06: x 0000 # data[1] = 0
+06-08: x 0000 # data[2] = 0
+08-10: x 0000 # data[3] = 0
+10-12: x 0000 # data[4] = 0
+12-14: x 0a00 # data[5] = 10
+14-16: x 0000 # data[6] = 0
+16-18: x 0a00 # data[7] = 10
+18-20: x e803 # data[8] = 1000
+20-22: x ffff # data[9] = 65535
 Keeping b32 open
 Keeping b64 open
 
@@ -224,15 +228,15 @@ write
 size rows=(11, 10, 9, 8)
 ----
 b32:
-  32: *colblk.UintBuilder[uint32].Size(11, 0) = 44
-  32: *colblk.UintBuilder[uint32].Size(10, 0) = 24
-  32: *colblk.UintBuilder[uint32].Size(9, 0) = 22
-  32: *colblk.UintBuilder[uint32].Size(8, 0) = 12
+  32: *colblk.UintBuilder[uint32].Size(11, 0) = 48
+  32: *colblk.UintBuilder[uint32].Size(10, 0) = 26
+  32: *colblk.UintBuilder[uint32].Size(9, 0) = 24
+  32: *colblk.UintBuilder[uint32].Size(8, 0) = 13
 b64:
-  64: *colblk.UintBuilder[uint64].Size(11, 0) = 52
-  64: *colblk.UintBuilder[uint64].Size(10, 0) = 28
-  64: *colblk.UintBuilder[uint64].Size(9, 0) = 26
-  64: *colblk.UintBuilder[uint64].Size(8, 0) = 16
+  64: *colblk.UintBuilder[uint64].Size(11, 0) = 56
+  64: *colblk.UintBuilder[uint64].Size(10, 0) = 30
+  64: *colblk.UintBuilder[uint64].Size(9, 0) = 28
+  64: *colblk.UintBuilder[uint64].Size(8, 0) = 17
 
 # We should be able to write up to 2^32-1 without triggering a 64-bit encoding.
 
@@ -243,35 +247,38 @@ write
 size rows=(12, 11, 10, 9, 8)
 ----
 b32:
-  32: *colblk.UintBuilder[uint32].Size(12, 0) = 48
-  32: *colblk.UintBuilder[uint32].Size(11, 0) = 44
-  32: *colblk.UintBuilder[uint32].Size(10, 0) = 24
-  32: *colblk.UintBuilder[uint32].Size(9, 0) = 22
-  32: *colblk.UintBuilder[uint32].Size(8, 0) = 12
+  32: *colblk.UintBuilder[uint32].Size(12, 0) = 52
+  32: *colblk.UintBuilder[uint32].Size(11, 0) = 48
+  32: *colblk.UintBuilder[uint32].Size(10, 0) = 26
+  32: *colblk.UintBuilder[uint32].Size(9, 0) = 24
+  32: *colblk.UintBuilder[uint32].Size(8, 0) = 13
 b64:
-  64: *colblk.UintBuilder[uint64].Size(12, 0) = 56
-  64: *colblk.UintBuilder[uint64].Size(11, 0) = 52
-  64: *colblk.UintBuilder[uint64].Size(10, 0) = 28
-  64: *colblk.UintBuilder[uint64].Size(9, 0) = 26
-  64: *colblk.UintBuilder[uint64].Size(8, 0) = 16
+  64: *colblk.UintBuilder[uint64].Size(12, 0) = 60
+  64: *colblk.UintBuilder[uint64].Size(11, 0) = 56
+  64: *colblk.UintBuilder[uint64].Size(10, 0) = 30
+  64: *colblk.UintBuilder[uint64].Size(9, 0) = 28
+  64: *colblk.UintBuilder[uint64].Size(8, 0) = 17
 
 # But 2^32 should trigger a 64-bit encoding.
 
 finish widths=(32) rows=12
 ----
 b32: *colblk.UintBuilder[uint32]:
-00-04: x 00000000 # data[0] = 0
-04-08: x 00000000 # data[1] = 0
-08-12: x 00000000 # data[2] = 0
-12-16: x 00000000 # data[3] = 0
-16-20: x 00000000 # data[4] = 0
-20-24: x 0a000000 # data[5] = 10
-24-28: x 00000000 # data[6] = 0
-28-32: x 0a000000 # data[7] = 10
-32-36: x e8030000 # data[8] = 1000
-36-40: x ffff0000 # data[9] = 65535
-40-44: x 00000100 # data[10] = 65536
-44-48: x ffffffff # data[11] = 4294967295
+00-01: x 00       # delta encoding: none
+# padding
+01-04: x 000000   # aligning to 32-bit boundary
+04-08: x 00000000 # data[0] = 0
+08-12: x 00000000 # data[1] = 0
+12-16: x 00000000 # data[2] = 0
+16-20: x 00000000 # data[3] = 0
+20-24: x 00000000 # data[4] = 0
+24-28: x 0a000000 # data[5] = 10
+28-32: x 00000000 # data[6] = 0
+32-36: x 0a000000 # data[7] = 10
+36-40: x e8030000 # data[8] = 1000
+40-44: x ffff0000 # data[9] = 65535
+44-48: x 00000100 # data[10] = 65536
+48-52: x ffffffff # data[11] = 4294967295
 Keeping b64 open
 
 write
@@ -281,29 +288,32 @@ write
 size rows=(13, 12, 11, 10, 9, 8)
 ----
 b64:
-  64: *colblk.UintBuilder[uint64].Size(13, 0) = 104
-  64: *colblk.UintBuilder[uint64].Size(12, 0) = 56
-  64: *colblk.UintBuilder[uint64].Size(11, 0) = 52
-  64: *colblk.UintBuilder[uint64].Size(10, 0) = 28
-  64: *colblk.UintBuilder[uint64].Size(9, 0) = 26
-  64: *colblk.UintBuilder[uint64].Size(8, 0) = 16
+  64: *colblk.UintBuilder[uint64].Size(13, 0) = 112
+  64: *colblk.UintBuilder[uint64].Size(12, 0) = 60
+  64: *colblk.UintBuilder[uint64].Size(11, 0) = 56
+  64: *colblk.UintBuilder[uint64].Size(10, 0) = 30
+  64: *colblk.UintBuilder[uint64].Size(9, 0) = 28
+  64: *colblk.UintBuilder[uint64].Size(8, 0) = 17
 
 finish widths=(64) rows=13
 ----
 b64: *colblk.UintBuilder[uint64]:
-000-008: x 0000000000000000 # data[0] = 0
-008-016: x 0000000000000000 # data[1] = 0
-016-024: x 0000000000000000 # data[2] = 0
-024-032: x 0000000000000000 # data[3] = 0
-032-040: x 0000000000000000 # data[4] = 0
-040-048: x 0a00000000000000 # data[5] = 10
-048-056: x 0000000000000000 # data[6] = 0
-056-064: x 0a00000000000000 # data[7] = 10
-064-072: x e803000000000000 # data[8] = 1000
-072-080: x ffff000000000000 # data[9] = 65535
-080-088: x 0000010000000000 # data[10] = 65536
-088-096: x ffffffff00000000 # data[11] = 4294967295
-096-104: x 0000000001000000 # data[12] = 4294967296
+000-001: x 00               # delta encoding: none
+# padding
+001-008: x 00000000000000   # aligning to 64-bit boundary
+008-016: x 0000000000000000 # data[0] = 0
+016-024: x 0000000000000000 # data[1] = 0
+024-032: x 0000000000000000 # data[2] = 0
+032-040: x 0000000000000000 # data[3] = 0
+040-048: x 0000000000000000 # data[4] = 0
+048-056: x 0a00000000000000 # data[5] = 10
+056-064: x 0000000000000000 # data[6] = 0
+064-072: x 0a00000000000000 # data[7] = 10
+072-080: x e803000000000000 # data[8] = 1000
+080-088: x ffff000000000000 # data[9] = 65535
+088-096: x 0000010000000000 # data[10] = 65536
+096-104: x ffffffff00000000 # data[11] = 4294967295
+104-112: x 0000000001000000 # data[12] = 4294967296
 
 # Repeat the above tests but with a zero default value, and without explicitly
 # setting any of the zero values.
@@ -321,32 +331,32 @@ b64
 size rows=(5, 4, 3, 2, 1, 0)
 ----
 b8:
-  8: *colblk.UintBuilder[uint8].Size(5, 0) = 1
-  8: *colblk.UintBuilder[uint8].Size(4, 0) = 1
-  8: *colblk.UintBuilder[uint8].Size(3, 0) = 1
-  8: *colblk.UintBuilder[uint8].Size(2, 0) = 1
-  8: *colblk.UintBuilder[uint8].Size(1, 0) = 1
+  8: *colblk.UintBuilder[uint8].Size(5, 0) = 2
+  8: *colblk.UintBuilder[uint8].Size(4, 0) = 2
+  8: *colblk.UintBuilder[uint8].Size(3, 0) = 2
+  8: *colblk.UintBuilder[uint8].Size(2, 0) = 2
+  8: *colblk.UintBuilder[uint8].Size(1, 0) = 2
   8: *colblk.UintBuilder[uint8].Size(0, 0) = 0
 b16:
-  16: *colblk.UintBuilder[uint16].Size(5, 0) = 2
-  16: *colblk.UintBuilder[uint16].Size(4, 0) = 2
-  16: *colblk.UintBuilder[uint16].Size(3, 0) = 2
-  16: *colblk.UintBuilder[uint16].Size(2, 0) = 2
-  16: *colblk.UintBuilder[uint16].Size(1, 0) = 2
+  16: *colblk.UintBuilder[uint16].Size(5, 0) = 3
+  16: *colblk.UintBuilder[uint16].Size(4, 0) = 3
+  16: *colblk.UintBuilder[uint16].Size(3, 0) = 3
+  16: *colblk.UintBuilder[uint16].Size(2, 0) = 3
+  16: *colblk.UintBuilder[uint16].Size(1, 0) = 3
   16: *colblk.UintBuilder[uint16].Size(0, 0) = 0
 b32:
-  32: *colblk.UintBuilder[uint32].Size(5, 0) = 4
-  32: *colblk.UintBuilder[uint32].Size(4, 0) = 4
-  32: *colblk.UintBuilder[uint32].Size(3, 0) = 4
-  32: *colblk.UintBuilder[uint32].Size(2, 0) = 4
-  32: *colblk.UintBuilder[uint32].Size(1, 0) = 4
+  32: *colblk.UintBuilder[uint32].Size(5, 0) = 5
+  32: *colblk.UintBuilder[uint32].Size(4, 0) = 5
+  32: *colblk.UintBuilder[uint32].Size(3, 0) = 5
+  32: *colblk.UintBuilder[uint32].Size(2, 0) = 5
+  32: *colblk.UintBuilder[uint32].Size(1, 0) = 5
   32: *colblk.UintBuilder[uint32].Size(0, 0) = 0
 b64:
-  64: *colblk.UintBuilder[uint64].Size(5, 0) = 8
-  64: *colblk.UintBuilder[uint64].Size(4, 0) = 8
-  64: *colblk.UintBuilder[uint64].Size(3, 0) = 8
-  64: *colblk.UintBuilder[uint64].Size(2, 0) = 8
-  64: *colblk.UintBuilder[uint64].Size(1, 0) = 8
+  64: *colblk.UintBuilder[uint64].Size(5, 0) = 9
+  64: *colblk.UintBuilder[uint64].Size(4, 0) = 9
+  64: *colblk.UintBuilder[uint64].Size(3, 0) = 9
+  64: *colblk.UintBuilder[uint64].Size(2, 0) = 9
+  64: *colblk.UintBuilder[uint64].Size(1, 0) = 9
   64: *colblk.UintBuilder[uint64].Size(0, 0) = 0
 
 # Add a couple nonzero values. Size calls that include the new row count should
@@ -360,58 +370,59 @@ write
 size rows=(8, 7, 6, 5, 4, 3, 2, 1, 0)
 ----
 b8:
-  8: *colblk.UintBuilder[uint8].Size(8, 0) = 8
-  8: *colblk.UintBuilder[uint8].Size(7, 0) = 7
-  8: *colblk.UintBuilder[uint8].Size(6, 0) = 6
-  8: *colblk.UintBuilder[uint8].Size(5, 0) = 1
-  8: *colblk.UintBuilder[uint8].Size(4, 0) = 1
-  8: *colblk.UintBuilder[uint8].Size(3, 0) = 1
-  8: *colblk.UintBuilder[uint8].Size(2, 0) = 1
-  8: *colblk.UintBuilder[uint8].Size(1, 0) = 1
+  8: *colblk.UintBuilder[uint8].Size(8, 0) = 9
+  8: *colblk.UintBuilder[uint8].Size(7, 0) = 8
+  8: *colblk.UintBuilder[uint8].Size(6, 0) = 7
+  8: *colblk.UintBuilder[uint8].Size(5, 0) = 2
+  8: *colblk.UintBuilder[uint8].Size(4, 0) = 2
+  8: *colblk.UintBuilder[uint8].Size(3, 0) = 2
+  8: *colblk.UintBuilder[uint8].Size(2, 0) = 2
+  8: *colblk.UintBuilder[uint8].Size(1, 0) = 2
   8: *colblk.UintBuilder[uint8].Size(0, 0) = 0
 b16:
-  16: *colblk.UintBuilder[uint16].Size(8, 0) = 10
-  16: *colblk.UintBuilder[uint16].Size(7, 0) = 9
-  16: *colblk.UintBuilder[uint16].Size(6, 0) = 8
-  16: *colblk.UintBuilder[uint16].Size(5, 0) = 2
-  16: *colblk.UintBuilder[uint16].Size(4, 0) = 2
-  16: *colblk.UintBuilder[uint16].Size(3, 0) = 2
-  16: *colblk.UintBuilder[uint16].Size(2, 0) = 2
-  16: *colblk.UintBuilder[uint16].Size(1, 0) = 2
+  16: *colblk.UintBuilder[uint16].Size(8, 0) = 11
+  16: *colblk.UintBuilder[uint16].Size(7, 0) = 10
+  16: *colblk.UintBuilder[uint16].Size(6, 0) = 9
+  16: *colblk.UintBuilder[uint16].Size(5, 0) = 3
+  16: *colblk.UintBuilder[uint16].Size(4, 0) = 3
+  16: *colblk.UintBuilder[uint16].Size(3, 0) = 3
+  16: *colblk.UintBuilder[uint16].Size(2, 0) = 3
+  16: *colblk.UintBuilder[uint16].Size(1, 0) = 3
   16: *colblk.UintBuilder[uint16].Size(0, 0) = 0
 b32:
-  32: *colblk.UintBuilder[uint32].Size(8, 0) = 12
-  32: *colblk.UintBuilder[uint32].Size(7, 0) = 11
-  32: *colblk.UintBuilder[uint32].Size(6, 0) = 10
-  32: *colblk.UintBuilder[uint32].Size(5, 0) = 4
-  32: *colblk.UintBuilder[uint32].Size(4, 0) = 4
-  32: *colblk.UintBuilder[uint32].Size(3, 0) = 4
-  32: *colblk.UintBuilder[uint32].Size(2, 0) = 4
-  32: *colblk.UintBuilder[uint32].Size(1, 0) = 4
+  32: *colblk.UintBuilder[uint32].Size(8, 0) = 13
+  32: *colblk.UintBuilder[uint32].Size(7, 0) = 12
+  32: *colblk.UintBuilder[uint32].Size(6, 0) = 11
+  32: *colblk.UintBuilder[uint32].Size(5, 0) = 5
+  32: *colblk.UintBuilder[uint32].Size(4, 0) = 5
+  32: *colblk.UintBuilder[uint32].Size(3, 0) = 5
+  32: *colblk.UintBuilder[uint32].Size(2, 0) = 5
+  32: *colblk.UintBuilder[uint32].Size(1, 0) = 5
   32: *colblk.UintBuilder[uint32].Size(0, 0) = 0
 b64:
-  64: *colblk.UintBuilder[uint64].Size(8, 0) = 16
-  64: *colblk.UintBuilder[uint64].Size(7, 0) = 15
-  64: *colblk.UintBuilder[uint64].Size(6, 0) = 14
-  64: *colblk.UintBuilder[uint64].Size(5, 0) = 8
-  64: *colblk.UintBuilder[uint64].Size(4, 0) = 8
-  64: *colblk.UintBuilder[uint64].Size(3, 0) = 8
-  64: *colblk.UintBuilder[uint64].Size(2, 0) = 8
-  64: *colblk.UintBuilder[uint64].Size(1, 0) = 8
+  64: *colblk.UintBuilder[uint64].Size(8, 0) = 17
+  64: *colblk.UintBuilder[uint64].Size(7, 0) = 16
+  64: *colblk.UintBuilder[uint64].Size(6, 0) = 15
+  64: *colblk.UintBuilder[uint64].Size(5, 0) = 9
+  64: *colblk.UintBuilder[uint64].Size(4, 0) = 9
+  64: *colblk.UintBuilder[uint64].Size(3, 0) = 9
+  64: *colblk.UintBuilder[uint64].Size(2, 0) = 9
+  64: *colblk.UintBuilder[uint64].Size(1, 0) = 9
   64: *colblk.UintBuilder[uint64].Size(0, 0) = 0
 
 # Finish the b8 so we can test 16-bit encoding.
 finish widths=(8) rows=8
 ----
 b8: *colblk.UintBuilder[uint8]:
-0-1: x 00 # data[0] = 0
-1-2: x 00 # data[1] = 0
-2-3: x 00 # data[2] = 0
-3-4: x 00 # data[3] = 0
-4-5: x 00 # data[4] = 0
-5-6: x 0a # data[5] = 10
-6-7: x 00 # data[6] = 0
-7-8: x 0a # data[7] = 10
+0-1: x 00 # delta encoding: none
+1-2: x 00 # data[0] = 0
+2-3: x 00 # data[1] = 0
+3-4: x 00 # data[2] = 0
+4-5: x 00 # data[3] = 0
+5-6: x 00 # data[4] = 0
+6-7: x 0a # data[5] = 10
+7-8: x 00 # data[6] = 0
+8-9: x 0a # data[7] = 10
 Keeping b16 open
 Keeping b32 open
 Keeping b64 open
@@ -425,14 +436,14 @@ write
 size rows=(9, 8)
 ----
 b16:
-  16: *colblk.UintBuilder[uint16].Size(9, 0) = 18
-  16: *colblk.UintBuilder[uint16].Size(8, 0) = 10
+  16: *colblk.UintBuilder[uint16].Size(9, 0) = 20
+  16: *colblk.UintBuilder[uint16].Size(8, 0) = 11
 b32:
-  32: *colblk.UintBuilder[uint32].Size(9, 0) = 22
-  32: *colblk.UintBuilder[uint32].Size(8, 0) = 12
+  32: *colblk.UintBuilder[uint32].Size(9, 0) = 24
+  32: *colblk.UintBuilder[uint32].Size(8, 0) = 13
 b64:
-  64: *colblk.UintBuilder[uint64].Size(9, 0) = 26
-  64: *colblk.UintBuilder[uint64].Size(8, 0) = 16
+  64: *colblk.UintBuilder[uint64].Size(9, 0) = 28
+  64: *colblk.UintBuilder[uint64].Size(8, 0) = 17
 
 # We should be able to write up to 2^16-1 without triggering a 32-bit encoding.
 
@@ -443,17 +454,17 @@ write
 size rows=(10, 9, 8)
 ----
 b16:
-  16: *colblk.UintBuilder[uint16].Size(10, 0) = 20
-  16: *colblk.UintBuilder[uint16].Size(9, 0) = 18
-  16: *colblk.UintBuilder[uint16].Size(8, 0) = 10
+  16: *colblk.UintBuilder[uint16].Size(10, 0) = 22
+  16: *colblk.UintBuilder[uint16].Size(9, 0) = 20
+  16: *colblk.UintBuilder[uint16].Size(8, 0) = 11
 b32:
-  32: *colblk.UintBuilder[uint32].Size(10, 0) = 24
-  32: *colblk.UintBuilder[uint32].Size(9, 0) = 22
-  32: *colblk.UintBuilder[uint32].Size(8, 0) = 12
+  32: *colblk.UintBuilder[uint32].Size(10, 0) = 26
+  32: *colblk.UintBuilder[uint32].Size(9, 0) = 24
+  32: *colblk.UintBuilder[uint32].Size(8, 0) = 13
 b64:
-  64: *colblk.UintBuilder[uint64].Size(10, 0) = 28
-  64: *colblk.UintBuilder[uint64].Size(9, 0) = 26
-  64: *colblk.UintBuilder[uint64].Size(8, 0) = 16
+  64: *colblk.UintBuilder[uint64].Size(10, 0) = 30
+  64: *colblk.UintBuilder[uint64].Size(9, 0) = 28
+  64: *colblk.UintBuilder[uint64].Size(8, 0) = 17
 
 # But 2^16 should trigger a 32-bit encoding. (Finish b16 so we can test 32-bit
 # encoding.)
@@ -461,16 +472,19 @@ b64:
 finish widths=(16) rows=10
 ----
 b16: *colblk.UintBuilder[uint16]:
-00-02: x 0000 # data[0] = 0
-02-04: x 0000 # data[1] = 0
-04-06: x 0000 # data[2] = 0
-06-08: x 0000 # data[3] = 0
-08-10: x 0000 # data[4] = 0
-10-12: x 0a00 # data[5] = 10
-12-14: x 0000 # data[6] = 0
-14-16: x 0a00 # data[7] = 10
-16-18: x e803 # data[8] = 1000
-18-20: x ffff # data[9] = 65535
+00-01: x 00   # delta encoding: none
+# padding
+01-02: x 00   # aligning to 16-bit boundary
+02-04: x 0000 # data[0] = 0
+04-06: x 0000 # data[1] = 0
+06-08: x 0000 # data[2] = 0
+08-10: x 0000 # data[3] = 0
+10-12: x 0000 # data[4] = 0
+12-14: x 0a00 # data[5] = 10
+14-16: x 0000 # data[6] = 0
+16-18: x 0a00 # data[7] = 10
+18-20: x e803 # data[8] = 1000
+20-22: x ffff # data[9] = 65535
 Keeping b32 open
 Keeping b64 open
 
@@ -481,15 +495,15 @@ write
 size rows=(11, 10, 9, 8)
 ----
 b32:
-  32: *colblk.UintBuilder[uint32].Size(11, 0) = 44
-  32: *colblk.UintBuilder[uint32].Size(10, 0) = 24
-  32: *colblk.UintBuilder[uint32].Size(9, 0) = 22
-  32: *colblk.UintBuilder[uint32].Size(8, 0) = 12
+  32: *colblk.UintBuilder[uint32].Size(11, 0) = 48
+  32: *colblk.UintBuilder[uint32].Size(10, 0) = 26
+  32: *colblk.UintBuilder[uint32].Size(9, 0) = 24
+  32: *colblk.UintBuilder[uint32].Size(8, 0) = 13
 b64:
-  64: *colblk.UintBuilder[uint64].Size(11, 0) = 52
-  64: *colblk.UintBuilder[uint64].Size(10, 0) = 28
-  64: *colblk.UintBuilder[uint64].Size(9, 0) = 26
-  64: *colblk.UintBuilder[uint64].Size(8, 0) = 16
+  64: *colblk.UintBuilder[uint64].Size(11, 0) = 56
+  64: *colblk.UintBuilder[uint64].Size(10, 0) = 30
+  64: *colblk.UintBuilder[uint64].Size(9, 0) = 28
+  64: *colblk.UintBuilder[uint64].Size(8, 0) = 17
 
 # We should be able to write up to 2^32-1 without triggering a 64-bit encoding.
 
@@ -500,35 +514,38 @@ write
 size rows=(12, 11, 10, 9, 8)
 ----
 b32:
-  32: *colblk.UintBuilder[uint32].Size(12, 0) = 48
-  32: *colblk.UintBuilder[uint32].Size(11, 0) = 44
-  32: *colblk.UintBuilder[uint32].Size(10, 0) = 24
-  32: *colblk.UintBuilder[uint32].Size(9, 0) = 22
-  32: *colblk.UintBuilder[uint32].Size(8, 0) = 12
+  32: *colblk.UintBuilder[uint32].Size(12, 0) = 52
+  32: *colblk.UintBuilder[uint32].Size(11, 0) = 48
+  32: *colblk.UintBuilder[uint32].Size(10, 0) = 26
+  32: *colblk.UintBuilder[uint32].Size(9, 0) = 24
+  32: *colblk.UintBuilder[uint32].Size(8, 0) = 13
 b64:
-  64: *colblk.UintBuilder[uint64].Size(12, 0) = 56
-  64: *colblk.UintBuilder[uint64].Size(11, 0) = 52
-  64: *colblk.UintBuilder[uint64].Size(10, 0) = 28
-  64: *colblk.UintBuilder[uint64].Size(9, 0) = 26
-  64: *colblk.UintBuilder[uint64].Size(8, 0) = 16
+  64: *colblk.UintBuilder[uint64].Size(12, 0) = 60
+  64: *colblk.UintBuilder[uint64].Size(11, 0) = 56
+  64: *colblk.UintBuilder[uint64].Size(10, 0) = 30
+  64: *colblk.UintBuilder[uint64].Size(9, 0) = 28
+  64: *colblk.UintBuilder[uint64].Size(8, 0) = 17
 
 # But 2^32 should trigger a 64-bit encoding.
 
 finish widths=(32) rows=12
 ----
 b32: *colblk.UintBuilder[uint32]:
-00-04: x 00000000 # data[0] = 0
-04-08: x 00000000 # data[1] = 0
-08-12: x 00000000 # data[2] = 0
-12-16: x 00000000 # data[3] = 0
-16-20: x 00000000 # data[4] = 0
-20-24: x 0a000000 # data[5] = 10
-24-28: x 00000000 # data[6] = 0
-28-32: x 0a000000 # data[7] = 10
-32-36: x e8030000 # data[8] = 1000
-36-40: x ffff0000 # data[9] = 65535
-40-44: x 00000100 # data[10] = 65536
-44-48: x ffffffff # data[11] = 4294967295
+00-01: x 00       # delta encoding: none
+# padding
+01-04: x 000000   # aligning to 32-bit boundary
+04-08: x 00000000 # data[0] = 0
+08-12: x 00000000 # data[1] = 0
+12-16: x 00000000 # data[2] = 0
+16-20: x 00000000 # data[3] = 0
+20-24: x 00000000 # data[4] = 0
+24-28: x 0a000000 # data[5] = 10
+28-32: x 00000000 # data[6] = 0
+32-36: x 0a000000 # data[7] = 10
+36-40: x e8030000 # data[8] = 1000
+40-44: x ffff0000 # data[9] = 65535
+44-48: x 00000100 # data[10] = 65536
+48-52: x ffffffff # data[11] = 4294967295
 Keeping b64 open
 
 write
@@ -538,29 +555,32 @@ write
 size rows=(13, 12, 11, 10, 9, 8)
 ----
 b64:
-  64: *colblk.UintBuilder[uint64].Size(13, 0) = 104
-  64: *colblk.UintBuilder[uint64].Size(12, 0) = 56
-  64: *colblk.UintBuilder[uint64].Size(11, 0) = 52
-  64: *colblk.UintBuilder[uint64].Size(10, 0) = 28
-  64: *colblk.UintBuilder[uint64].Size(9, 0) = 26
-  64: *colblk.UintBuilder[uint64].Size(8, 0) = 16
+  64: *colblk.UintBuilder[uint64].Size(13, 0) = 112
+  64: *colblk.UintBuilder[uint64].Size(12, 0) = 60
+  64: *colblk.UintBuilder[uint64].Size(11, 0) = 56
+  64: *colblk.UintBuilder[uint64].Size(10, 0) = 30
+  64: *colblk.UintBuilder[uint64].Size(9, 0) = 28
+  64: *colblk.UintBuilder[uint64].Size(8, 0) = 17
 
 finish widths=(64) rows=13
 ----
 b64: *colblk.UintBuilder[uint64]:
-000-008: x 0000000000000000 # data[0] = 0
-008-016: x 0000000000000000 # data[1] = 0
-016-024: x 0000000000000000 # data[2] = 0
-024-032: x 0000000000000000 # data[3] = 0
-032-040: x 0000000000000000 # data[4] = 0
-040-048: x 0a00000000000000 # data[5] = 10
-048-056: x 0000000000000000 # data[6] = 0
-056-064: x 0a00000000000000 # data[7] = 10
-064-072: x e803000000000000 # data[8] = 1000
-072-080: x ffff000000000000 # data[9] = 65535
-080-088: x 0000010000000000 # data[10] = 65536
-088-096: x ffffffff00000000 # data[11] = 4294967295
-096-104: x 0000000001000000 # data[12] = 4294967296
+000-001: x 00               # delta encoding: none
+# padding
+001-008: x 00000000000000   # aligning to 64-bit boundary
+008-016: x 0000000000000000 # data[0] = 0
+016-024: x 0000000000000000 # data[1] = 0
+024-032: x 0000000000000000 # data[2] = 0
+032-040: x 0000000000000000 # data[3] = 0
+040-048: x 0000000000000000 # data[4] = 0
+048-056: x 0a00000000000000 # data[5] = 10
+056-064: x 0000000000000000 # data[6] = 0
+064-072: x 0a00000000000000 # data[7] = 10
+072-080: x e803000000000000 # data[8] = 1000
+080-088: x ffff000000000000 # data[9] = 65535
+088-096: x 0000010000000000 # data[10] = 65536
+096-104: x ffffffff00000000 # data[11] = 4294967295
+104-112: x 0000000001000000 # data[12] = 4294967296
 
 # Test serializing a few columns using delta encoding.
 
@@ -578,43 +598,47 @@ write
 size rows=5
 ----
 b8:
-  8: *colblk.UintBuilder[uint8].Size(5, 0) = 5
+  8: *colblk.UintBuilder[uint8].Size(5, 0) = 6
 b16:
-  16: *colblk.UintBuilder[uint16].Size(5, 0) = 7
+  16: *colblk.UintBuilder[uint16].Size(5, 0) = 8
 b32:
-  32: *colblk.UintBuilder[uint32].Size(5, 0) = 9
+  32: *colblk.UintBuilder[uint32].Size(5, 0) = 10
 b64:
-  64: *colblk.UintBuilder[uint64].Size(5, 0) = 13
+  64: *colblk.UintBuilder[uint64].Size(5, 0) = 14
 
 finish widths=(8,16,32,64) rows=5
 ----
 b8: *colblk.UintBuilder[uint8]:
-0-1: x 01 # data[0] = 1
-1-2: x 00 # data[1] = 0
-2-3: x 5c # data[2] = 92
-3-4: x 01 # data[3] = 1
-4-5: x 00 # data[4] = 0
+0-1: x 00 # delta encoding: none
+1-2: x 01 # data[0] = 1
+2-3: x 00 # data[1] = 0
+3-4: x 5c # data[2] = 92
+4-5: x 01 # data[3] = 1
+5-6: x 00 # data[4] = 0
 b16: *colblk.UintBuilder[uint16]:
-0-2: x 0000 # 16-bit constant: 0
-2-3: x 01   # data[0] = 1
-3-4: x 00   # data[1] = 0
-4-5: x 5c   # data[2] = 92
-5-6: x 01   # data[3] = 1
-6-7: x 00   # data[4] = 0
+0-1: x 02   # delta encoding: delta8
+1-3: x 0000 # 16-bit constant: 0
+3-4: x 01   # data[0] = 1
+4-5: x 00   # data[1] = 0
+5-6: x 5c   # data[2] = 92
+6-7: x 01   # data[3] = 1
+7-8: x 00   # data[4] = 0
 b32: *colblk.UintBuilder[uint32]:
-0-4: x 00000000 # 32-bit constant: 0
-4-5: x 01       # data[0] = 1
-5-6: x 00       # data[1] = 0
-6-7: x 5c       # data[2] = 92
-7-8: x 01       # data[3] = 1
-8-9: x 00       # data[4] = 0
+0-1: x 02       # delta encoding: delta8
+1-5: x 00000000 # 32-bit constant: 0
+5-6: x 01       # data[0] = 1
+6-7: x 00       # data[1] = 0
+7-8: x 5c       # data[2] = 92
+8-9: x 01       # data[3] = 1
+9-10: x 00      # data[4] = 0
 b64: *colblk.UintBuilder[uint64]:
-00-08: x 0000000000000000 # 64-bit constant: 0
-08-09: x 01               # data[0] = 1
-09-10: x 00               # data[1] = 0
-10-11: x 5c               # data[2] = 92
-11-12: x 01               # data[3] = 1
-12-13: x 00               # data[4] = 0
+00-01: x 02               # delta encoding: delta8
+01-09: x 0000000000000000 # 64-bit constant: 0
+09-10: x 01               # data[0] = 1
+10-11: x 00               # data[1] = 0
+11-12: x 5c               # data[2] = 92
+12-13: x 01               # data[3] = 1
+13-14: x 00               # data[4] = 0
 
 # Test a situation where the most recently written value requirs a wider delta
 # encoding, but we Finish with few enough rows that we should serialize using
@@ -631,7 +655,7 @@ write
 size rows=(6)
 ----
 b64:
-  64: *colblk.UintBuilder[uint64].Size(6, 0) = 20
+  64: *colblk.UintBuilder[uint64].Size(6, 0) = 22
 
 write
 6:70395
@@ -640,18 +664,21 @@ write
 size rows=(7)
 ----
 b64:
-  64: *colblk.UintBuilder[uint64].Size(7, 0) = 36
+  64: *colblk.UintBuilder[uint64].Size(7, 0) = 40
 
 finish widths=(64) rows=6
 ----
 b64: *colblk.UintBuilder[uint64]:
-00-08: x 0000000000000000 # 64-bit constant: 0
-08-10: x 0000             # data[0] = 0
-10-12: x 1d00             # data[1] = 29
-12-14: x 5302             # data[2] = 595
-14-16: x 0200             # data[3] = 2
-16-18: x 0200             # data[4] = 2
-18-20: x 0900             # data[5] = 9
+00-01: x 03               # delta encoding: delta16
+01-09: x 0000000000000000 # 64-bit constant: 0
+# padding
+09-10: x 00               # aligning to 16-bit boundary
+10-12: x 0000             # data[0] = 0
+12-14: x 1d00             # data[1] = 29
+14-16: x 5302             # data[2] = 595
+16-18: x 0200             # data[3] = 2
+18-20: x 0200             # data[4] = 2
+20-22: x 0900             # data[5] = 9
 
 # Test the constant encoded.
 
@@ -666,12 +693,13 @@ write
 size rows=(6)
 ----
 b64:
-  64: *colblk.UintBuilder[uint64].Size(6, 0) = 8
+  64: *colblk.UintBuilder[uint64].Size(6, 0) = 9
 
 finish widths=(64) rows=6
 ----
 b64: *colblk.UintBuilder[uint64]:
-0-8: x 0100000000000000 # 64-bit constant: 1
+0-1: x 01               # delta encoding: const
+1-9: x 0100000000000000 # 64-bit constant: 1
 
 # Test 32-bit delta encoding.
 
@@ -692,9 +720,10 @@ finish widths=(64) rows=3 offset=1
 ----
 b64: *colblk.UintBuilder[uint64]:
 00-01: x 00               # artificial start offset
-01-09: x 0100000000000000 # 64-bit constant: 1
-# Padding
-09-12: x 000000           # aligning to 32-bit boundary
+01-02: x 04               # delta encoding: delta32
+02-10: x 0100000000000000 # 64-bit constant: 1
+# padding
+10-12: x 0000             # aligning to 32-bit boundary
 12-16: x 00000000         # data[0] = 0
 16-20: x bff90000         # data[1] = 63935
 20-24: x c31f2d00         # data[2] = 2957251
diff --git a/sstable/colblk/uints_test.go b/sstable/colblk/uints_test.go
index 4a21d193..f6e6fc22 100644
--- a/sstable/colblk/uints_test.go
+++ b/sstable/colblk/uints_test.go
@@ -114,13 +114,13 @@ func TestUints(t *testing.T) {
 				if shouldFinish {
 					sz := writers[wIdx].Size(rows, offset)
 					buf := aligned.ByteSlice(int(sz))
-					_, desc := writers[wIdx].Finish(0, rows, offset, buf)
+					_ = writers[wIdx].Finish(0, rows, offset, buf)
 					fmt.Fprintf(&out, "b%d: %T:\n", width, writers[wIdx])
 					f := binfmt.New(buf).LineWidth(20)
 					if offset > 0 {
 						f.HexBytesln(int(offset), "artificial start offset")
 					}
-					uintsToBinFormatter(f, rows, desc, nil)
+					uintsToBinFormatter(f, rows, writers[wIdx].DataType(0), nil)
 					fmt.Fprintf(&out, "%s", f.String())
 				} else {
 					fmt.Fprintf(&out, "Keeping b%d open\n", width)