Repository: petermattis/pebble
Pull request #229 (2019-08-17 19:34:09)

Implement `manifest dump`
* Move Version, and VersionEdit to internal/manifest
* Cut circular dependency between version and versionSet


petermattis: 

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


Patch:
diff --git a/compaction.go b/compaction.go
index 0cc9d12..b2c28c9 100644
--- a/compaction.go
+++ b/compaction.go
@@ -15,6 +15,7 @@ import (
 	"unsafe"
 
 	"github.com/petermattis/pebble/internal/base"
+	"github.com/petermattis/pebble/internal/manifest"
 	"github.com/petermattis/pebble/internal/rangedel"
 	"github.com/petermattis/pebble/sstable"
 	"github.com/petermattis/pebble/vfs"
@@ -35,6 +36,14 @@ func maxGrandparentOverlapBytes(opts *Options, level int) uint64 {
 	return uint64(10 * opts.Level(level).TargetFileSize)
 }
 
+// totalSize returns the total size of all the files in f.
+func totalSize(f []fileMetadata) (size uint64) {
+	for _, x := range f {
+		size += x.Size
+	}
+	return size
+}
+
 // compaction is a table compaction from one level to the next, starting from a
 // given version.
 type compaction struct {
@@ -82,6 +91,8 @@ type compaction struct {
 	grandparents    []fileMetadata
 	overlappedBytes uint64 // bytes of overlap with grandparent tables
 	seenKey         bool   // some output key has been seen
+
+	metrics map[int]*LevelMetrics
 }
 
 func newCompaction(
@@ -187,7 +198,7 @@ func newFlush(
 			}
 		}
 
-		c.grandparents = c.version.overlaps(baseLevel, c.cmp, smallest.UserKey, largest.UserKey)
+		c.grandparents = c.version.Overlaps(baseLevel, c.cmp, smallest.UserKey, largest.UserKey)
 	}
 	return c
 }
@@ -196,18 +207,18 @@ func newFlush(
 // whether the compaction was automatically scheduled or user initiated.
 func (c *compaction) setupOtherInputs() {
 	c.inputs[0] = c.expandInputs(c.inputs[0])
-	smallest0, largest0 := ikeyRange(c.cmp, c.inputs[0], nil)
-	c.inputs[1] = c.version.overlaps(c.outputLevel, c.cmp, smallest0.UserKey, largest0.UserKey)
-	smallest01, largest01 := ikeyRange(c.cmp, c.inputs[0], c.inputs[1])
+	smallest0, largest0 := manifest.KeyRange(c.cmp, c.inputs[0], nil)
+	c.inputs[1] = c.version.Overlaps(c.outputLevel, c.cmp, smallest0.UserKey, largest0.UserKey)
+	smallest01, largest01 := manifest.KeyRange(c.cmp, c.inputs[0], c.inputs[1])
 
 	// Grow the inputs if it doesn't affect the number of level+1 files.
 	if c.grow(smallest01, largest01) {
-		smallest01, largest01 = ikeyRange(c.cmp, c.inputs[0], c.inputs[1])
+		smallest01, largest01 = manifest.KeyRange(c.cmp, c.inputs[0], c.inputs[1])
 	}
 
 	// Compute the set of outputLevel+1 files that overlap this compaction.
 	if c.outputLevel+1 < numLevels {
-		c.grandparents = c.version.overlaps(c.outputLevel+1, c.cmp, smallest01.UserKey, largest01.UserKey)
+		c.grandparents = c.version.Overlaps(c.outputLevel+1, c.cmp, smallest01.UserKey, largest01.UserKey)
 	}
 }
 
@@ -222,7 +233,7 @@ func (c *compaction) expandInputs(inputs []fileMetadata) []fileMetadata {
 		// get a "clean cut".
 		return inputs
 	}
-	files := c.version.files[c.startLevel]
+	files := c.version.Files[c.startLevel]
 	// Pointer arithmetic to figure out the index if inputs[0] with
 	// files[0]. This requires that the inputs slice is a sub-slice of
 	// files. This is true for non-L0 files returned from version.overlaps.
@@ -238,10 +249,10 @@ func (c *compaction) expandInputs(inputs []fileMetadata) []fileMetadata {
 	for ; end < len(files); end++ {
 		cur := &files[end-1]
 		next := &files[end]
-		if c.cmp(cur.largest.UserKey, next.smallest.UserKey) < 0 {
+		if c.cmp(cur.Largest.UserKey, next.Smallest.UserKey) < 0 {
 			break
 		}
-		if cur.largest.Trailer == InternalKeyRangeDeleteSentinel {
+		if cur.Largest.Trailer == InternalKeyRangeDeleteSentinel {
 			// The range deletion sentinel key is set for the largest key in a table
 			// when a range deletion tombstone straddles a table. It isn't necessary
 			// to include the next table in the compaction as cur.largest.UserKey
@@ -261,7 +272,7 @@ func (c *compaction) grow(sm, la InternalKey) bool {
 	if len(c.inputs[1]) == 0 {
 		return false
 	}
-	grow0 := c.version.overlaps(c.startLevel, c.cmp, sm.UserKey, la.UserKey)
+	grow0 := c.version.Overlaps(c.startLevel, c.cmp, sm.UserKey, la.UserKey)
 	grow0 = c.expandInputs(grow0)
 	if len(grow0) <= len(c.inputs[0]) {
 		return false
@@ -269,8 +280,8 @@ func (c *compaction) grow(sm, la InternalKey) bool {
 	if totalSize(grow0)+totalSize(c.inputs[1]) >= c.maxExpandedBytes {
 		return false
 	}
-	sm1, la1 := ikeyRange(c.cmp, grow0, nil)
-	grow1 := c.version.overlaps(c.outputLevel, c.cmp, sm1.UserKey, la1.UserKey)
+	sm1, la1 := manifest.KeyRange(c.cmp, grow0, nil)
+	grow1 := c.version.Overlaps(c.outputLevel, c.cmp, sm1.UserKey, la1.UserKey)
 	if len(grow1) != len(c.inputs[1]) {
 		return false
 	}
@@ -312,11 +323,11 @@ func (c *compaction) trivialMove() bool {
 func (c *compaction) shouldStopBefore(key InternalKey) bool {
 	for len(c.grandparents) > 0 {
 		g := &c.grandparents[0]
-		if base.InternalCompare(c.cmp, key, g.largest) <= 0 {
+		if base.InternalCompare(c.cmp, key, g.Largest) <= 0 {
 			break
 		}
 		if c.seenKey {
-			c.overlappedBytes += g.size
+			c.overlappedBytes += g.Size
 		}
 		c.grandparents = c.grandparents[1:]
 	}
@@ -334,7 +345,7 @@ func (c *compaction) shouldStopBefore(key InternalKey) bool {
 // lower level in the LSM.
 func (c *compaction) allowZeroSeqNum(iter internalIterator) bool {
 	if len(c.flushing) != 0 {
-		if len(c.version.files[0]) > 0 {
+		if len(c.version.Files[0]) > 0 {
 			// We can only allow zeroing of seqnum for L0 tables if no other L0 tables
 			// exist. Otherwise we may violate the invariant that L0 tables are ordered
 			// by increasing seqnum. This could be relaxed with a bit more intelligence
@@ -354,11 +365,11 @@ func (c *compaction) allowZeroSeqNum(iter internalIterator) bool {
 		files := c.inputs[i]
 		for j := range files {
 			f := &files[j]
-			if lower == nil || c.cmp(lower, f.smallest.UserKey) > 0 {
-				lower = f.smallest.UserKey
+			if lower == nil || c.cmp(lower, f.Smallest.UserKey) > 0 {
+				lower = f.Smallest.UserKey
 			}
-			if upper == nil || c.cmp(upper, f.largest.UserKey) < 0 {
-				upper = f.largest.UserKey
+			if upper == nil || c.cmp(upper, f.Largest.UserKey) < 0 {
+				upper = f.Largest.UserKey
 			}
 		}
 	}
@@ -386,9 +397,9 @@ func (c *compaction) elideTombstone(key []byte) bool {
 	// successive elideTombstones calls and we can keep some state in between
 	// calls.
 	for ; level < numLevels; level++ {
-		for _, f := range c.version.files[level] {
-			if c.cmp(key, f.largest.UserKey) <= 0 {
-				if c.cmp(key, f.smallest.UserKey) >= 0 {
+		for _, f := range c.version.Files[level] {
+			if c.cmp(key, f.Largest.UserKey) <= 0 {
+				if c.cmp(key, f.Smallest.UserKey) >= 0 {
 					return false
 				}
 				// For levels below level 0, the files within a level are in
@@ -417,7 +428,7 @@ func (c *compaction) elideRangeTombstone(start, end []byte) bool {
 	}
 
 	for ; level < numLevels; level++ {
-		overlaps := c.version.overlaps(level, c.cmp, start, end)
+		overlaps := c.version.Overlaps(level, c.cmp, start, end)
 		if len(overlaps) > 0 {
 			return false
 		}
@@ -432,14 +443,14 @@ func (c *compaction) atomicUnitBounds(f *fileMetadata) (lower, upper []byte) {
 		files := c.inputs[i]
 		for j := range files {
 			if f == &files[j] {
-				lowerBound := f.smallest.UserKey
+				lowerBound := f.Smallest.UserKey
 				for k := j; k > 0; k-- {
 					cur := &files[k]
 					prev := &files[k-1]
-					if c.cmp(prev.largest.UserKey, cur.smallest.UserKey) < 0 {
+					if c.cmp(prev.Largest.UserKey, cur.Smallest.UserKey) < 0 {
 						break
 					}
-					if prev.largest.Trailer == InternalKeyRangeDeleteSentinel {
+					if prev.Largest.Trailer == InternalKeyRangeDeleteSentinel {
 						// The range deletion sentinel key is set for the largest key in a
 						// table when a range deletion tombstone straddles a table. It
 						// isn't necessary to include the next table in the atomic
@@ -447,17 +458,17 @@ func (c *compaction) atomicUnitBounds(f *fileMetadata) (lower, upper []byte) {
 						// in the table.
 						break
 					}
-					lowerBound = prev.smallest.UserKey
+					lowerBound = prev.Smallest.UserKey
 				}
 
-				upperBound := f.largest.UserKey
+				upperBound := f.Largest.UserKey
 				for k := j + 1; k < len(files); k++ {
 					cur := &files[k-1]
 					next := &files[k]
-					if c.cmp(cur.largest.UserKey, next.smallest.UserKey) < 0 {
+					if c.cmp(cur.Largest.UserKey, next.Smallest.UserKey) < 0 {
 						break
 					}
-					if cur.largest.Trailer == InternalKeyRangeDeleteSentinel {
+					if cur.Largest.Trailer == InternalKeyRangeDeleteSentinel {
 						// The range deletion sentinel key is set for the largest key in a
 						// table when a range deletion tombstone straddles a table. It
 						// isn't necessary to include the next table in the atomic
@@ -467,7 +478,7 @@ func (c *compaction) atomicUnitBounds(f *fileMetadata) (lower, upper []byte) {
 					}
 					// cur.largest.UserKey == next.largest.UserKey, so next is part of
 					// the atomic compaction unit.
-					upperBound = next.largest.UserKey
+					upperBound = next.Largest.UserKey
 				}
 				return lowerBound, upperBound
 			}
@@ -553,7 +564,7 @@ func (c *compaction) newInputIter(
 			f := &c.inputs[0][i]
 			iter, rangeDelIter, err := newIters(f, nil /* iter options */, &c.bytesIterated)
 			if err != nil {
-				return nil, fmt.Errorf("pebble: could not open table %d: %v", f.fileNum, err)
+				return nil, fmt.Errorf("pebble: could not open table %d: %v", f.FileNum, err)
 			}
 			iters = append(iters, iter)
 			if rangeDelIter != nil {
@@ -580,7 +591,7 @@ func (c *compaction) String() string {
 		}
 		fmt.Fprintf(&buf, "%d:", level)
 		for _, f := range c.inputs[i] {
-			fmt.Fprintf(&buf, " %d:%s-%s", f.fileNum, f.smallest, f.largest)
+			fmt.Fprintf(&buf, " %d:%s-%s", f.FileNum, f.Smallest, f.Largest)
 		}
 		fmt.Fprintf(&buf, "\n")
 	}
@@ -693,11 +704,11 @@ func (d *DB) flush1() error {
 			Err:   err,
 		}
 		if err == nil {
-			for i := range ve.newFiles {
-				e := &ve.newFiles[i]
-				info.Output = append(info.Output, e.meta.tableInfo(d.dirname))
+			for i := range ve.NewFiles {
+				e := &ve.NewFiles[i]
+				info.Output = append(info.Output, e.Meta.TableInfo(d.dirname))
 			}
-			if len(ve.newFiles) == 0 {
+			if len(ve.NewFiles) == 0 {
 				info.Err = errEmptyTable
 			}
 		}
@@ -710,14 +721,14 @@ func (d *DB) flush1() error {
 
 	// The flush succeeded or it produced an empty sstable. In either case we
 	// want to bump the log number.
-	ve.logNumber, _ = d.mu.mem.queue[n].logInfo()
-	metrics := ve.metrics[0]
+	ve.LogNum, _ = d.mu.mem.queue[n].logInfo()
+	metrics := c.metrics[0]
 	for i := 0; i < n; i++ {
 		_, size := d.mu.mem.queue[i].logInfo()
 		metrics.BytesIn += size
 	}
 
-	err = d.mu.versions.logAndApply(jobID, ve, d.dataDir)
+	err = d.mu.versions.logAndApply(jobID, ve, c.metrics, d.dataDir)
 	for _, fileNum := range pendingOutputs {
 		if _, ok := d.mu.compact.pendingOutputs[fileNum]; !ok {
 			panic("pebble: expected pending output not present")
@@ -819,7 +830,7 @@ func (d *DB) compact1() (err error) {
 		for i := range c.inputs {
 			for j := range c.inputs[i] {
 				m := &c.inputs[i][j]
-				info.Input.Tables[i] = append(info.Input.Tables[i], m.tableInfo(d.dirname))
+				info.Input.Tables[i] = append(info.Input.Tables[i], m.TableInfo(d.dirname))
 			}
 		}
 	}
@@ -837,9 +848,9 @@ func (d *DB) compact1() (err error) {
 	if d.opts.EventListener.CompactionEnd != nil {
 		info.Err = err
 		if err == nil {
-			for i := range ve.newFiles {
-				e := &ve.newFiles[i]
-				info.Output.Tables = append(info.Output.Tables, e.meta.tableInfo(d.dirname))
+			for i := range ve.NewFiles {
+				e := &ve.NewFiles[i]
+				info.Output.Tables = append(info.Output.Tables, e.Meta.TableInfo(d.dirname))
 			}
 		}
 		d.opts.EventListener.CompactionEnd(info)
@@ -848,7 +859,7 @@ func (d *DB) compact1() (err error) {
 	if err != nil {
 		return err
 	}
-	err = d.mu.versions.logAndApply(jobID, ve, d.dataDir)
+	err = d.mu.versions.logAndApply(jobID, ve, c.metrics, d.dataDir)
 	for _, fileNum := range pendingOutputs {
 		if _, ok := d.mu.compact.pendingOutputs[fileNum]; !ok {
 			panic("pebble: expected pending output not present")
@@ -878,19 +889,20 @@ func (d *DB) runCompaction(jobID int, c *compaction, pacer pacer) (
 	// merge later on.
 	if c.trivialMove() {
 		meta := &c.inputs[0][0]
-		return &versionEdit{
-			deletedFiles: map[deletedFileEntry]bool{
-				deletedFileEntry{level: c.startLevel, fileNum: meta.fileNum}: true,
+		c.metrics = map[int]*LevelMetrics{
+			c.outputLevel: &LevelMetrics{
+				BytesMoved: meta.Size,
 			},
-			newFiles: []newFileEntry{
-				{level: c.outputLevel, meta: *meta},
+		}
+		ve := &versionEdit{
+			DeletedFiles: map[deletedFileEntry]bool{
+				deletedFileEntry{Level: c.startLevel, FileNum: meta.FileNum}: true,
 			},
-			metrics: map[int]*LevelMetrics{
-				c.outputLevel: &LevelMetrics{
-					BytesMoved: meta.size,
-				},
+			NewFiles: []newFileEntry{
+				{Level: c.outputLevel, Meta: *meta},
 			},
-		}, nil, nil
+		}
+		return ve, nil, nil
 	}
 
 	defer func() {
@@ -934,22 +946,22 @@ func (d *DB) runCompaction(jobID int, c *compaction, pacer pacer) (
 		}
 	}()
 
+	ve = &versionEdit{
+		DeletedFiles: map[deletedFileEntry]bool{},
+	}
+
 	metrics := &LevelMetrics{
 		BytesIn:   totalSize(c.inputs[0]),
 		BytesRead: totalSize(c.inputs[1]),
 	}
 	metrics.BytesRead += metrics.BytesIn
-
-	ve = &versionEdit{
-		deletedFiles: map[deletedFileEntry]bool{},
-		metrics: map[int]*LevelMetrics{
-			c.outputLevel: metrics,
-		},
+	c.metrics = map[int]*LevelMetrics{
+		c.outputLevel: metrics,
 	}
 
 	newOutput := func() error {
 		d.mu.Lock()
-		fileNum := d.mu.versions.nextFileNum()
+		fileNum := d.mu.versions.getNextFileNum()
 		d.mu.compact.pendingOutputs[fileNum] = struct{}{}
 		pendingOutputs = append(pendingOutputs, fileNum)
 		d.mu.Unlock()
@@ -977,10 +989,10 @@ func (d *DB) runCompaction(jobID int, c *compaction, pacer pacer) (
 		filenames = append(filenames, filename)
 		tw = sstable.NewWriter(file, d.opts, d.opts.Level(c.outputLevel))
 
-		ve.newFiles = append(ve.newFiles, newFileEntry{
-			level: c.outputLevel,
-			meta: fileMetadata{
-				fileNum: fileNum,
+		ve.NewFiles = append(ve.NewFiles, newFileEntry{
+			Level: c.outputLevel,
+			Meta: fileMetadata{
+				FileNum: fileNum,
 			},
 		})
 		return nil
@@ -1015,20 +1027,20 @@ func (d *DB) runCompaction(jobID int, c *compaction, pacer pacer) (
 			return err
 		}
 		tw = nil
-		meta := &ve.newFiles[len(ve.newFiles)-1].meta
-		meta.size = writerMeta.Size
-		meta.smallestSeqNum = writerMeta.SmallestSeqNum
-		meta.largestSeqNum = writerMeta.LargestSeqNum
+		meta := &ve.NewFiles[len(ve.NewFiles)-1].Meta
+		meta.Size = writerMeta.Size
+		meta.SmallestSeqNum = writerMeta.SmallestSeqNum
+		meta.LargestSeqNum = writerMeta.LargestSeqNum
 
-		metrics.BytesWritten += meta.size
+		metrics.BytesWritten += meta.Size
 
 		// The handling of range boundaries is a bit complicated.
-		if n := len(ve.newFiles); n > 1 {
+		if n := len(ve.NewFiles); n > 1 {
 			// This is not the first output. Bound the smallest range key by the
 			// previous tables largest key.
-			prevMeta := &ve.newFiles[n-2].meta
+			prevMeta := &ve.NewFiles[n-2].Meta
 			if writerMeta.SmallestRange.UserKey != nil &&
-				d.cmp(writerMeta.SmallestRange.UserKey, prevMeta.largest.UserKey) <= 0 {
+				d.cmp(writerMeta.SmallestRange.UserKey, prevMeta.Largest.UserKey) <= 0 {
 				// The range boundary user key is less than or equal to the previous
 				// table's largest key. We need the tables to be key-space partitioned,
 				// so force the boundary to a key that we know is larger than the
@@ -1042,7 +1054,7 @@ func (d *DB) runCompaction(jobID int, c *compaction, pacer pacer) (
 				// (the previous file could not end with a key at seqnum zero if this
 				// file had a tombstone extending into it).
 				writerMeta.SmallestRange = base.MakeInternalKey(
-					prevMeta.largest.UserKey, 0, InternalKeyKindRangeDelete)
+					prevMeta.Largest.UserKey, 0, InternalKeyKindRangeDelete)
 			}
 		}
 
@@ -1053,8 +1065,8 @@ func (d *DB) runCompaction(jobID int, c *compaction, pacer pacer) (
 			}
 		}
 
-		meta.smallest = writerMeta.Smallest(d.cmp)
-		meta.largest = writerMeta.Largest(d.cmp)
+		meta.Smallest = writerMeta.Smallest(d.cmp)
+		meta.Largest = writerMeta.Largest(d.cmp)
 		return nil
 	}
 
@@ -1094,9 +1106,9 @@ func (d *DB) runCompaction(jobID int, c *compaction, pacer pacer) (
 			level = c.outputLevel
 		}
 		for _, f := range c.inputs[i] {
-			ve.deletedFiles[deletedFileEntry{
-				level:   level,
-				fileNum: f.fileNum,
+			ve.DeletedFiles[deletedFileEntry{
+				Level:   level,
+				FileNum: f.FileNum,
 			}] = true
 		}
 	}
@@ -1128,8 +1140,8 @@ func (d *DB) scanObsoleteFiles(list []string) {
 		liveFileNums[fileNum] = struct{}{}
 	}
 	d.mu.versions.addLiveFileNums(liveFileNums)
-	logNumber := d.mu.versions.logNumber
-	manifestFileNumber := d.mu.versions.manifestFileNumber
+	logNumber := d.mu.versions.logNum
+	manifestFileNumber := d.mu.versions.manifestFileNum
 
 	var obsoleteLogs []uint64
 	var obsoleteTables []uint64
@@ -1195,7 +1207,7 @@ func (d *DB) deleteObsoleteFiles(jobID int) {
 	for i := range d.mu.log.queue {
 		// NB: d.mu.versions.logNumber is the file number of the latest log that
 		// has had its contents persisted to the LSM.
-		if d.mu.log.queue[i] >= d.mu.versions.logNumber {
+		if d.mu.log.queue[i] >= d.mu.versions.logNum {
 			obsoleteLogs = d.mu.log.queue[:i]
 			d.mu.log.queue = d.mu.log.queue[i:]
 			d.mu.versions.metrics.WAL.Files -= int64(len(obsoleteLogs))
diff --git a/compaction_picker.go b/compaction_picker.go
index af5694b..5c7b64c 100644
--- a/compaction_picker.go
+++ b/compaction_picker.go
@@ -6,6 +6,8 @@ package pebble
 
 import (
 	"math"
+
+	"github.com/petermattis/pebble/internal/manifest"
 )
 
 // compactionPicker holds the state and logic for picking a compaction. A
@@ -62,10 +64,10 @@ func (p *compactionPicker) estimatedCompactionDebt(l0ExtraSize uint64) uint64 {
 		return 0
 	}
 
-	compactionDebt := totalSize(p.vers.files[0]) + l0ExtraSize
+	compactionDebt := totalSize(p.vers.Files[0]) + l0ExtraSize
 	bytesAddedToNextLevel := compactionDebt
 
-	levelSize := totalSize(p.vers.files[p.baseLevel])
+	levelSize := totalSize(p.vers.Files[p.baseLevel])
 	// estimatedL0CompactionSize is the estimated size of the L0 component in the
 	// current or next L0->LBase compaction. This is needed to estimate the number
 	// of L0->LBase compactions which will need to occur for the LSM tree to
@@ -81,7 +83,7 @@ func (p *compactionPicker) estimatedCompactionDebt(l0ExtraSize uint64) uint64 {
 	for level := p.baseLevel; level < numLevels-1; level++ {
 		levelSize += bytesAddedToNextLevel
 		bytesAddedToNextLevel = 0
-		nextLevelSize = totalSize(p.vers.files[level+1])
+		nextLevelSize = totalSize(p.vers.Files[level+1])
 		if levelSize > uint64(p.levelMaxBytes[level]) {
 			bytesAddedToNextLevel = levelSize - uint64(p.levelMaxBytes[level])
 			levelRatio := float64(nextLevelSize) / float64(levelSize)
@@ -98,7 +100,7 @@ func (p *compactionPicker) initLevelMaxBytes(v *version, opts *Options) {
 	firstNonEmptyLevel := -1
 	var bottomLevelSize int64
 	for level := 1; level < numLevels; level++ {
-		levelSize := int64(totalSize(v.files[level]))
+		levelSize := int64(totalSize(v.Files[level]))
 		if levelSize > 0 {
 			if firstNonEmptyLevel == -1 {
 				firstNonEmptyLevel = level
@@ -185,11 +187,11 @@ func (p *compactionPicker) initTarget(v *version, opts *Options) {
 	// wish to avoid too many files when the individual file size is small
 	// (perhaps because of a small write-buffer setting, or very high
 	// compression ratios, or lots of overwrites/deletions).
-	p.score = float64(len(v.files[0])) / float64(opts.L0CompactionThreshold)
+	p.score = float64(len(v.Files[0])) / float64(opts.L0CompactionThreshold)
 	p.level = 0
 
 	for level := 1; level < numLevels-1; level++ {
-		score := float64(totalSize(v.files[level])) / float64(p.levelMaxBytes[level])
+		score := float64(totalSize(v.Files[level])) / float64(p.levelMaxBytes[level])
 		if p.score < score {
 			p.score = score
 			p.level = level
@@ -220,11 +222,11 @@ func (p *compactionPicker) initTarget(v *version, opts *Options) {
 		// The current heuristic matches the RocksDB kOldestSmallestSeqFirst
 		// heuristic.
 		smallestSeqNum := uint64(math.MaxUint64)
-		files := v.files[p.level]
+		files := v.Files[p.level]
 		for i := range files {
 			f := &files[i]
-			if smallestSeqNum > f.smallestSeqNum {
-				smallestSeqNum = f.smallestSeqNum
+			if smallestSeqNum > f.SmallestSeqNum {
+				smallestSeqNum = f.SmallestSeqNum
 				p.file = i
 			}
 		}
@@ -233,10 +235,10 @@ func (p *compactionPicker) initTarget(v *version, opts *Options) {
 
 	// No levels exceeded their size threshold. Check for forced compactions.
 	for level := 0; level < numLevels-1; level++ {
-		files := v.files[p.level]
+		files := v.Files[p.level]
 		for i := range files {
 			f := &files[i]
-			if f.markedForCompaction {
+			if f.MarkedForCompaction {
 				p.score = 1.0
 				p.level = level
 				p.file = i
@@ -261,13 +263,13 @@ func (p *compactionPicker) pickAuto(
 
 	vers := p.vers
 	c = newCompaction(opts, vers, p.level, p.baseLevel, bytesCompacted)
-	c.inputs[0] = vers.files[c.startLevel][p.file : p.file+1]
+	c.inputs[0] = vers.Files[c.startLevel][p.file : p.file+1]
 
 	// Files in level 0 may overlap each other, so pick up all overlapping ones.
 	if c.startLevel == 0 {
 		cmp := opts.Comparer.Compare
-		smallest, largest := ikeyRange(cmp, c.inputs[0], nil)
-		c.inputs[0] = vers.overlaps(0, cmp, smallest.UserKey, largest.UserKey)
+		smallest, largest := manifest.KeyRange(cmp, c.inputs[0], nil)
+		c.inputs[0] = vers.Overlaps(0, cmp, smallest.UserKey, largest.UserKey)
 		if len(c.inputs[0]) == 0 {
 			panic("pebble: empty compaction")
 		}
@@ -291,7 +293,7 @@ func (p *compactionPicker) pickManual(
 	c = newCompaction(opts, cur, manual.level, p.baseLevel, bytesCompacted)
 	manual.outputLevel = c.outputLevel
 	cmp := opts.Comparer.Compare
-	c.inputs[0] = cur.overlaps(manual.level, cmp, manual.start.UserKey, manual.end.UserKey)
+	c.inputs[0] = cur.Overlaps(manual.level, cmp, manual.start.UserKey, manual.end.UserKey)
 	if len(c.inputs[0]) == 0 {
 		return nil
 	}
diff --git a/db.go b/db.go
index dc670ab..bce86fd 100644
--- a/db.go
+++ b/db.go
@@ -284,7 +284,7 @@ func (d *DB) getInternal(key []byte, b *Batch, s *Snapshot) ([]byte, error) {
 	get.key = key
 	get.batch = b
 	get.mem = readState.memtables
-	get.l0 = readState.current.files[0]
+	get.l0 = readState.current.Files[0]
 	get.version = readState.current
 
 	i := &buf.dbi
@@ -532,8 +532,8 @@ func (d *DB) newIterInternal(
 
 	// The level 0 files need to be added from newest to oldest.
 	current := readState.current
-	for i := len(current.files[0]) - 1; i >= 0; i-- {
-		f := &current.files[0][i]
+	for i := len(current.Files[0]) - 1; i >= 0; i-- {
+		f := &current.Files[0][i]
 		iter, rangeDelIter, err := d.newIters(f, &dbi.opts, nil)
 		if err != nil {
 			dbi.err = err
@@ -545,8 +545,8 @@ func (d *DB) newIterInternal(
 	}
 
 	start := len(rangeDelIters)
-	for level := 1; level < len(current.files); level++ {
-		if len(current.files[level]) == 0 {
+	for level := 1; level < len(current.Files); level++ {
+		if len(current.Files[level]) == 0 {
 			continue
 		}
 		rangeDelIters = append(rangeDelIters, nil)
@@ -559,8 +559,8 @@ func (d *DB) newIterInternal(
 
 	// Add level iterators for the remaining files.
 	levels := buf.levels[:]
-	for level := 1; level < len(current.files); level++ {
-		if len(current.files[level]) == 0 {
+	for level := 1; level < len(current.Files); level++ {
+		if len(current.Files[level]) == 0 {
 			continue
 		}
 
@@ -572,7 +572,7 @@ func (d *DB) newIterInternal(
 			li = &levelIter{}
 		}
 
-		li.init(&dbi.opts, d.cmp, d.newIters, current.files[level], nil)
+		li.init(&dbi.opts, d.cmp, d.newIters, current.Files[level], nil)
 		li.initRangeDel(&rangeDelIters[0])
 		li.initLargestUserKey(&largestUserKeys[0])
 		iters = append(iters, li)
@@ -667,8 +667,8 @@ func (d *DB) Close() error {
 		d.readState.val.unrefLocked()
 
 		current := d.mu.versions.currentVersion()
-		for v := d.mu.versions.versions.front(); true; v = v.next {
-			refs := atomic.LoadInt32(&v.refs)
+		for v := d.mu.versions.versions.Front(); true; v = v.Next() {
+			refs := v.Refs()
 			if v == current {
 				if refs != 1 {
 					return fmt.Errorf("leaked iterators: current\n%s", v)
@@ -694,13 +694,13 @@ func (d *DB) Compact(start, end []byte /* CompactionOptions */) error {
 
 	iStart := base.MakeInternalKey(start, InternalKeySeqNumMax, InternalKeyKindMax)
 	iEnd := base.MakeInternalKey(end, 0, 0)
-	meta := []*fileMetadata{&fileMetadata{smallest: iStart, largest: iEnd}}
+	meta := []*fileMetadata{&fileMetadata{Smallest: iStart, Largest: iEnd}}
 
 	d.mu.Lock()
 	maxLevelWithFiles := 1
 	cur := d.mu.versions.currentVersion()
 	for level := 0; level < numLevels; level++ {
-		if len(cur.overlaps(level, d.cmp, start, end)) > 0 {
+		if len(cur.Overlaps(level, d.cmp, start, end)) > 0 {
 			maxLevelWithFiles = level + 1
 		}
 	}
@@ -896,7 +896,7 @@ func (d *DB) makeRoomForWrite(b *Batch) error {
 			d.mu.compact.cond.Wait()
 			continue
 		}
-		if len(d.mu.versions.currentVersion().files[0]) > d.opts.L0StopWritesThreshold {
+		if len(d.mu.versions.currentVersion().Files[0]) > d.opts.L0StopWritesThreshold {
 			// There are too many level-0 files, so we wait.
 			if !stalled {
 				stalled = true
@@ -918,7 +918,7 @@ func (d *DB) makeRoomForWrite(b *Batch) error {
 		if !d.opts.DisableWAL {
 			jobID := d.mu.nextJobID
 			d.mu.nextJobID++
-			newLogNumber = d.mu.versions.nextFileNum()
+			newLogNumber = d.mu.versions.getNextFileNum()
 			d.mu.mem.switching = true
 			d.mu.Unlock()
 
diff --git a/get_iter.go b/get_iter.go
index 6a65eb5..1af54cf 100644
--- a/get_iter.go
+++ b/get_iter.go
@@ -134,7 +134,7 @@ func (g *getIter) Next() (*InternalKey, []byte) {
 				l := &g.l0[n-1]
 				g.iter, g.rangeDelIter, g.err = g.newIters(
 					l,
-					nil /* iter options */,
+					nil, /* iter options */
 					nil /* bytes iterated */)
 				if g.err != nil {
 					return nil, nil
@@ -149,12 +149,12 @@ func (g *getIter) Next() (*InternalKey, []byte) {
 		if g.level >= numLevels {
 			return nil, nil
 		}
-		if len(g.version.files[g.level]) == 0 {
+		if len(g.version.Files[g.level]) == 0 {
 			g.level++
 			continue
 		}
 
-		g.levelIter.init(nil, g.cmp, g.newIters, g.version.files[g.level], nil)
+		g.levelIter.init(nil, g.cmp, g.newIters, g.version.Files[g.level], nil)
 		g.levelIter.initRangeDel(&g.rangeDelIter)
 		g.level++
 		g.iter = &g.levelIter
diff --git a/ingest.go b/ingest.go
index 149f693..4b40826 100644
--- a/ingest.go
+++ b/ingest.go
@@ -46,21 +46,21 @@ func ingestLoad1(opts *Options, path string, dbNum, fileNum uint64) (*fileMetada
 	}
 
 	meta := &fileMetadata{}
-	meta.fileNum = fileNum
-	meta.size = uint64(stat.Size())
-	meta.smallest = InternalKey{}
-	meta.largest = InternalKey{}
+	meta.FileNum = fileNum
+	meta.Size = uint64(stat.Size())
+	meta.Smallest = InternalKey{}
+	meta.Largest = InternalKey{}
 	smallestSet, largestSet := false, false
 
 	{
 		iter := r.NewIter(nil /* lower */, nil /* upper */)
 		defer iter.Close()
 		if key, _ := iter.First(); key != nil {
-			meta.smallest = key.Clone()
+			meta.Smallest = key.Clone()
 			smallestSet = true
 		}
 		if key, _ := iter.Last(); key != nil {
-			meta.largest = key.Clone()
+			meta.Largest = key.Clone()
 			largestSet = true
 		}
 		if err := iter.Error(); err != nil {
@@ -72,15 +72,15 @@ func ingestLoad1(opts *Options, path string, dbNum, fileNum uint64) (*fileMetada
 		defer iter.Close()
 		if key, _ := iter.First(); key != nil {
 			if !smallestSet ||
-				base.InternalCompare(opts.Comparer.Compare, meta.smallest, *key) > 0 {
-				meta.smallest = key.Clone()
+				base.InternalCompare(opts.Comparer.Compare, meta.Smallest, *key) > 0 {
+				meta.Smallest = key.Clone()
 			}
 		}
 		if key, val := iter.Last(); key != nil {
 			end := base.MakeRangeDeleteSentinelKey(val)
 			if !largestSet ||
-				base.InternalCompare(opts.Comparer.Compare, meta.largest, end) < 0 {
-				meta.largest = end.Clone()
+				base.InternalCompare(opts.Comparer.Compare, meta.Largest, end) < 0 {
+				meta.Largest = end.Clone()
 			}
 		}
 	}
@@ -108,11 +108,11 @@ func ingestSortAndVerify(cmp Compare, meta []*fileMetadata) error {
 	}
 
 	sort.Slice(meta, func(i, j int) bool {
-		return cmp(meta[i].smallest.UserKey, meta[j].smallest.UserKey) < 0
+		return cmp(meta[i].Smallest.UserKey, meta[j].Smallest.UserKey) < 0
 	})
 
 	for i := 1; i < len(meta); i++ {
-		if sstableKeyCompare(cmp, meta[i-1].largest, meta[i].smallest) >= 0 {
+		if sstableKeyCompare(cmp, meta[i-1].Largest, meta[i].Smallest) >= 0 {
 			return fmt.Errorf("files have overlapping ranges")
 		}
 	}
@@ -122,7 +122,7 @@ func ingestSortAndVerify(cmp Compare, meta []*fileMetadata) error {
 func ingestCleanup(fs vfs.FS, dirname string, meta []*fileMetadata) error {
 	var firstErr error
 	for i := range meta {
-		target := base.MakeFilename(dirname, fileTypeTable, meta[i].fileNum)
+		target := base.MakeFilename(dirname, fileTypeTable, meta[i].FileNum)
 		if err := fs.Remove(target); err != nil {
 			if firstErr != nil {
 				firstErr = err
@@ -134,7 +134,7 @@ func ingestCleanup(fs vfs.FS, dirname string, meta []*fileMetadata) error {
 
 func ingestLink(opts *Options, dirname string, paths []string, meta []*fileMetadata) error {
 	for i := range paths {
-		target := base.MakeFilename(dirname, fileTypeTable, meta[i].fileNum)
+		target := base.MakeFilename(dirname, fileTypeTable, meta[i].FileNum)
 		err := opts.FS.Link(paths[i], target)
 		if err != nil {
 			if err2 := ingestCleanup(opts.FS, dirname, meta[:i]); err2 != nil {
@@ -154,11 +154,11 @@ func ingestMemtableOverlaps(cmp Compare, mem flushable, meta []*fileMetadata) bo
 		defer iter.Close()
 
 		for _, m := range meta {
-			key, _ := iter.SeekGE(m.smallest.UserKey)
+			key, _ := iter.SeekGE(m.Smallest.UserKey)
 			if key == nil {
 				continue
 			}
-			if cmp(key.UserKey, m.largest.UserKey) <= 0 {
+			if cmp(key.UserKey, m.Largest.UserKey) <= 0 {
 				return true
 			}
 		}
@@ -168,17 +168,17 @@ func ingestMemtableOverlaps(cmp Compare, mem flushable, meta []*fileMetadata) bo
 	if iter := mem.newRangeDelIter(nil); iter != nil {
 		defer iter.Close()
 		for _, m := range meta {
-			key, val := iter.SeekLT(m.smallest.UserKey)
+			key, val := iter.SeekLT(m.Smallest.UserKey)
 			if key == nil {
 				key, val = iter.Next()
 			}
 			for ; key != nil; key, val = iter.Next() {
-				if cmp(key.UserKey, m.largest.UserKey) > 0 {
+				if cmp(key.UserKey, m.Largest.UserKey) > 0 {
 					// The start of the tombstone is after the largest key in the
 					// ingested table.
 					break
 				}
-				if cmp(val, m.smallest.UserKey) > 0 {
+				if cmp(val, m.Smallest.UserKey) > 0 {
 					// The end of the tombstone is greater than the smallest in the
 					// table. Note that the tombstone end key is exclusive, thus ">0"
 					// instead of ">=0".
@@ -193,12 +193,12 @@ func ingestMemtableOverlaps(cmp Compare, mem flushable, meta []*fileMetadata) bo
 
 func ingestUpdateSeqNum(opts *Options, dirname string, seqNum uint64, meta []*fileMetadata) error {
 	for _, m := range meta {
-		m.smallest = base.MakeInternalKey(m.smallest.UserKey, seqNum, m.smallest.Kind())
-		m.largest = base.MakeInternalKey(m.largest.UserKey, seqNum, m.largest.Kind())
+		m.Smallest = base.MakeInternalKey(m.Smallest.UserKey, seqNum, m.Smallest.Kind())
+		m.Largest = base.MakeInternalKey(m.Largest.UserKey, seqNum, m.Largest.Kind())
 		// Setting smallestSeqNum == largestSeqNum triggers the setting of
 		// Properties.GlobalSeqNum when an sstable is loaded.
-		m.smallestSeqNum = seqNum
-		m.largestSeqNum = seqNum
+		m.SmallestSeqNum = seqNum
+		m.LargestSeqNum = seqNum
 		seqNum++
 
 		// TODO(peter): Update the global sequence number property. This is only
@@ -209,13 +209,13 @@ func ingestUpdateSeqNum(opts *Options, dirname string, seqNum uint64, meta []*fi
 
 func ingestTargetLevel(cmp Compare, v *version, meta *fileMetadata) int {
 	// Find the lowest level which does not have any files which overlap meta.
-	if len(v.overlaps(0, cmp, meta.smallest.UserKey, meta.largest.UserKey)) != 0 {
+	if len(v.Overlaps(0, cmp, meta.Smallest.UserKey, meta.Largest.UserKey)) != 0 {
 		return 0
 	}
 
 	level := 1
 	for ; level < numLevels; level++ {
-		if len(v.overlaps(level, cmp, meta.smallest.UserKey, meta.largest.UserKey)) != 0 {
+		if len(v.Overlaps(level, cmp, meta.Smallest.UserKey, meta.Largest.UserKey)) != 0 {
 			break
 		}
 	}
@@ -265,7 +265,7 @@ func (d *DB) Ingest(paths []string) error {
 	d.mu.Lock()
 	pendingOutputs := make([]uint64, len(paths))
 	for i := range paths {
-		pendingOutputs[i] = d.mu.versions.nextFileNum()
+		pendingOutputs[i] = d.mu.versions.getNextFileNum()
 	}
 	for _, fileNum := range pendingOutputs {
 		d.mu.compact.pendingOutputs[fileNum] = struct{}{}
@@ -369,18 +369,18 @@ func (d *DB) Ingest(paths []string) error {
 	if d.opts.EventListener.TableIngested != nil {
 		info := TableIngestInfo{
 			JobID:        jobID,
-			GlobalSeqNum: meta[0].smallestSeqNum,
+			GlobalSeqNum: meta[0].SmallestSeqNum,
 			Err:          err,
 		}
 		if ve != nil {
 			info.Tables = make([]struct {
 				TableInfo
 				Level int
-			}, len(ve.newFiles))
-			for i := range ve.newFiles {
-				e := &ve.newFiles[i]
-				info.Tables[i].Level = e.level
-				info.Tables[i].TableInfo = e.meta.tableInfo(d.dirname)
+			}, len(ve.NewFiles))
+			for i := range ve.NewFiles {
+				e := &ve.NewFiles[i]
+				info.Tables[i].Level = e.Level
+				info.Tables[i].TableInfo = e.Meta.TableInfo(d.dirname)
 			}
 		}
 		d.opts.EventListener.TableIngested(info)
@@ -394,25 +394,25 @@ func (d *DB) ingestApply(jobID int, meta []*fileMetadata) (*versionEdit, error)
 	defer d.mu.Unlock()
 
 	ve := &versionEdit{
-		newFiles: make([]newFileEntry, len(meta)),
-		metrics:  make(map[int]*LevelMetrics),
+		NewFiles: make([]newFileEntry, len(meta)),
 	}
+	metrics := make(map[int]*LevelMetrics)
 	current := d.mu.versions.currentVersion()
 	for i := range meta {
 		// Determine the lowest level in the LSM for which the sstable doesn't
 		// overlap any existing files in the level.
 		m := meta[i]
-		f := &ve.newFiles[i]
-		f.level = ingestTargetLevel(d.cmp, current, m)
-		f.meta = *m
-		metrics := ve.metrics[f.level]
-		if metrics == nil {
-			metrics = &LevelMetrics{}
-			ve.metrics[f.level] = metrics
+		f := &ve.NewFiles[i]
+		f.Level = ingestTargetLevel(d.cmp, current, m)
+		f.Meta = *m
+		levelMetrics := metrics[f.Level]
+		if levelMetrics == nil {
+			levelMetrics = &LevelMetrics{}
+			metrics[f.Level] = levelMetrics
 		}
-		metrics.BytesIngested += m.size
+		levelMetrics.BytesIngested += m.Size
 	}
-	if err := d.mu.versions.logAndApply(jobID, ve, d.dataDir); err != nil {
+	if err := d.mu.versions.logAndApply(jobID, ve, metrics, d.dataDir); err != nil {
 		return nil, err
 	}
 	d.updateReadStateLocked()
diff --git a/version.go b/internal/manifest/version.go
similarity index 50%
rename from version.go
rename to internal/manifest/version.go
index 8561292..56db33f 100644
--- a/version.go
+++ b/internal/manifest/version.go
@@ -2,7 +2,7 @@
 // of this source code is governed by a BSD-style license that can be found in
 // the LICENSE file.
 
-package pebble
+package manifest
 
 import (
 	"bytes"
@@ -14,108 +14,119 @@ import (
 	"github.com/petermattis/pebble/internal/base"
 )
 
-// fileMetadata holds the metadata for an on-disk table.
-type fileMetadata struct {
+type Compare = base.Compare
+type InternalKey = base.InternalKey
+type Options = base.Options
+type TableInfo = base.TableInfo
+
+// FileMetadata holds the metadata for an on-disk table.
+type FileMetadata struct {
 	// reference count for the file: incremented when a file is added to a
 	// version and decremented when the version is unreferenced. The file is
 	// obsolete when the reference count falls to zero. This is a pointer because
 	// fileMetadata is copied by value from version to version, but we want the
 	// reference count to be shared.
 	refs *int32
-	// fileNum is the file number.
-	fileNum uint64
-	// size is the size of the file, in bytes.
-	size uint64
-	// smallest and largest are the inclusive bounds for the internal keys
+	// FileNum is the file number.
+	FileNum uint64
+	// Size is the Size of the file, in bytes.
+	Size uint64
+	// Smallest and Largest are the inclusive bounds for the internal keys
 	// stored in the table.
-	smallest InternalKey
-	largest  InternalKey
-	// smallest and largest sequence numbers in the table.
-	smallestSeqNum uint64
-	largestSeqNum  uint64
+	Smallest InternalKey
+	Largest  InternalKey
+	// Smallest and largest sequence numbers in the table.
+	SmallestSeqNum uint64
+	LargestSeqNum  uint64
 	// true if client asked us nicely to compact this file.
-	markedForCompaction bool
+	MarkedForCompaction bool
 }
 
-func (m *fileMetadata) String() string {
-	return fmt.Sprintf("%d:%s-%s", m.fileNum, m.smallest, m.largest)
+func (m *FileMetadata) String() string {
+	return fmt.Sprintf("%d:%s-%s", m.FileNum, m.Smallest, m.Largest)
 }
 
-func (m *fileMetadata) tableInfo(dirname string) TableInfo {
+// TableInfo returns a subset of the FileMetadata state formatted as a
+// TableInfo.
+func (m *FileMetadata) TableInfo(dirname string) TableInfo {
 	return TableInfo{
-		Path:           base.MakeFilename(dirname, fileTypeTable, m.fileNum),
-		FileNum:        m.fileNum,
-		Size:           m.size,
-		Smallest:       m.smallest,
-		Largest:        m.largest,
-		SmallestSeqNum: m.smallestSeqNum,
-		LargestSeqNum:  m.largestSeqNum,
-	}
-}
-
-// totalSize returns the total size of all the files in f.
-func totalSize(f []fileMetadata) (size uint64) {
-	for _, x := range f {
-		size += x.size
+		Path:           base.MakeFilename(dirname, base.FileTypeTable, m.FileNum),
+		FileNum:        m.FileNum,
+		Size:           m.Size,
+		Smallest:       m.Smallest,
+		Largest:        m.Largest,
+		SmallestSeqNum: m.SmallestSeqNum,
+		LargestSeqNum:  m.LargestSeqNum,
 	}
-	return size
 }
 
-// ikeyRange returns the minimum smallest and maximum largest internalKey for
+// KeyRange returns the minimum smallest and maximum largest internalKey for
 // all the fileMetadata in f0 and f1.
-func ikeyRange(ucmp Compare, f0, f1 []fileMetadata) (smallest, largest InternalKey) {
+func KeyRange(ucmp Compare, f0, f1 []FileMetadata) (smallest, largest InternalKey) {
 	first := true
-	for _, f := range [2][]fileMetadata{f0, f1} {
+	for _, f := range [2][]FileMetadata{f0, f1} {
 		for _, meta := range f {
 			if first {
 				first = false
-				smallest, largest = meta.smallest, meta.largest
+				smallest, largest = meta.Smallest, meta.Largest
 				continue
 			}
-			if base.InternalCompare(ucmp, meta.smallest, smallest) < 0 {
-				smallest = meta.smallest
+			if base.InternalCompare(ucmp, meta.Smallest, smallest) < 0 {
+				smallest = meta.Smallest
 			}
-			if base.InternalCompare(ucmp, meta.largest, largest) > 0 {
-				largest = meta.largest
+			if base.InternalCompare(ucmp, meta.Largest, largest) > 0 {
+				largest = meta.Largest
 			}
 		}
 	}
 	return smallest, largest
 }
 
-type bySeqNum []fileMetadata
+type bySeqNum []FileMetadata
 
 func (b bySeqNum) Len() int { return len(b) }
 func (b bySeqNum) Less(i, j int) bool {
 	// NB: This is the same ordering that RocksDB uses for L0 files.
 
 	// Sort first by largest sequence number.
-	if b[i].largestSeqNum != b[j].largestSeqNum {
-		return b[i].largestSeqNum < b[j].largestSeqNum
+	if b[i].LargestSeqNum != b[j].LargestSeqNum {
+		return b[i].LargestSeqNum < b[j].LargestSeqNum
 	}
 	// Then by smallest sequence number.
-	if b[i].smallestSeqNum != b[j].smallestSeqNum {
-		return b[i].smallestSeqNum < b[j].smallestSeqNum
+	if b[i].SmallestSeqNum != b[j].SmallestSeqNum {
+		return b[i].SmallestSeqNum < b[j].SmallestSeqNum
 	}
 	// Break ties by file number.
-	return b[i].fileNum < b[j].fileNum
+	return b[i].FileNum < b[j].FileNum
 }
 func (b bySeqNum) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
 
+// SortBySeqNum sorts the specified files by decreasing sequence number.
+func SortBySeqNum(files []FileMetadata) {
+	sort.Sort(bySeqNum(files))
+}
+
 type bySmallest struct {
-	dat []fileMetadata
+	dat []FileMetadata
 	cmp Compare
 }
 
 func (b bySmallest) Len() int { return len(b.dat) }
 func (b bySmallest) Less(i, j int) bool {
-	return base.InternalCompare(b.cmp, b.dat[i].smallest, b.dat[j].smallest) < 0
+	return base.InternalCompare(b.cmp, b.dat[i].Smallest, b.dat[j].Smallest) < 0
 }
 func (b bySmallest) Swap(i, j int) { b.dat[i], b.dat[j] = b.dat[j], b.dat[i] }
 
-const numLevels = 7
+// SortBySmallest sorts the specified files by smallest key using the supplied
+// comparison function to order user keys.
+func SortBySmallest(files []FileMetadata, cmp Compare) {
+	sort.Sort(bySmallest{files, cmp})
+}
+
+// NumLevels is the number of levels a Version contains.
+const NumLevels = 7
 
-// version is a collection of file metadata for on-disk tables at various
+// Version is a collection of file metadata for on-disk tables at various
 // levels. In-memory DBs are written to level-0 tables, and compactions
 // migrate data from level N to level N+1. The tables map internal keys (which
 // are a user key, a delete or set bit, and a sequence number) to user values.
@@ -135,106 +146,128 @@ const numLevels = 7
 // Finally, for every internal key in a table at level X, there is no internal
 // key in a higher level table that has both the same user key and a higher
 // sequence number.
-type version struct {
+type Version struct {
 	refs int32
 
-	files [numLevels][]fileMetadata
+	Files [NumLevels][]FileMetadata
 
-	// The version set this version is associated with.
-	vs *versionSet
+	// The callback to invoke when the last reference to a version is
+	// removed. Will be called with list.mu held.
+	Deleted func(obsolete []uint64)
 
 	// The list the version is linked into.
-	list *versionList
+	list *VersionList
 
 	// The next/prev link for the versionList doubly-linked list of versions.
-	prev, next *version
+	prev, next *Version
 }
 
-func (v *version) String() string {
+func (v *Version) String() string {
 	var buf bytes.Buffer
-	for level := 0; level < numLevels; level++ {
-		if len(v.files[level]) == 0 {
+	for level := 0; level < NumLevels; level++ {
+		if len(v.Files[level]) == 0 {
 			continue
 		}
 		fmt.Fprintf(&buf, "%d:", level)
-		for j := range v.files[level] {
-			f := &v.files[level][j]
-			fmt.Fprintf(&buf, " %s-%s", f.smallest.UserKey, f.largest.UserKey)
+		for j := range v.Files[level] {
+			f := &v.Files[level][j]
+			fmt.Fprintf(&buf, " %s-%s", f.Smallest.UserKey, f.Largest.UserKey)
 		}
 		fmt.Fprintf(&buf, "\n")
 	}
 	return buf.String()
 }
 
-func (v *version) DebugString() string {
+// DebugString returns an alternative format to String() which includes
+// sequence number and kind information for the sstable boundaries.
+func (v *Version) DebugString() string {
 	var buf bytes.Buffer
-	for level := 0; level < numLevels; level++ {
-		if len(v.files[level]) == 0 {
+	for level := 0; level < NumLevels; level++ {
+		if len(v.Files[level]) == 0 {
 			continue
 		}
 		fmt.Fprintf(&buf, "%d:", level)
-		for j := range v.files[level] {
-			f := &v.files[level][j]
-			fmt.Fprintf(&buf, " %s-%s", f.smallest, f.largest)
+		for j := range v.Files[level] {
+			f := &v.Files[level][j]
+			fmt.Fprintf(&buf, " %s-%s", f.Smallest, f.Largest)
 		}
 		fmt.Fprintf(&buf, "\n")
 	}
 	return buf.String()
 }
 
-func (v *version) ref() {
+// Refs returns the number of references to the version.
+func (v *Version) Refs() int32 {
+	return atomic.LoadInt32(&v.refs)
+}
+
+// Ref increments the version refcount.
+func (v *Version) Ref() {
 	atomic.AddInt32(&v.refs, 1)
 }
 
-func (v *version) unref() {
+// Unref decrements the version refcount. If the last reference to the version
+// was removed, the version is removed from the list of versions and the
+// Deleted callback is invoked. Requires that the VersionList mutex is NOT
+// locked.
+func (v *Version) Unref() {
 	if atomic.AddInt32(&v.refs, -1) == 0 {
 		obsolete := v.unrefFiles()
 		l := v.list
 		l.mu.Lock()
-		l.remove(v)
-		v.vs.addObsoleteLocked(obsolete)
+		l.Remove(v)
+		v.Deleted(obsolete)
 		l.mu.Unlock()
 	}
 }
 
-func (v *version) unrefLocked() {
+// UnrefLocked decrements the version refcount. If the last reference to the
+// version was removed, the version is removed from the list of versions and
+// the Deleted callback is invoked. Requires that the VersionList mutex is
+// already locked.
+func (v *Version) UnrefLocked() {
 	if atomic.AddInt32(&v.refs, -1) == 0 {
-		v.list.remove(v)
-		v.vs.addObsoleteLocked(v.unrefFiles())
+		v.list.Remove(v)
+		v.Deleted(v.unrefFiles())
 	}
 }
 
-func (v *version) unrefFiles() []uint64 {
+func (v *Version) unrefFiles() []uint64 {
 	var obsolete []uint64
-	for _, files := range v.files {
+	for _, files := range v.Files {
 		for i := range files {
 			f := &files[i]
 			if atomic.AddInt32(f.refs, -1) == 0 {
-				obsolete = append(obsolete, f.fileNum)
+				obsolete = append(obsolete, f.FileNum)
 			}
 		}
 	}
 	return obsolete
 }
 
-// overlaps returns all elements of v.files[level] whose user key range
+// Next returns the next version in the list of versions.
+func (v *Version) Next() *Version {
+	return v.next
+}
+
+// Overlaps returns all elements of v.files[level] whose user key range
 // intersects the inclusive range [start, end]. If level is non-zero then the
 // user key ranges of v.files[level] are assumed to not overlap (although they
 // may touch). If level is zero then that assumption cannot be made, and the
 // [start, end] range is expanded to the union of those matching ranges so far
 // and the computation is repeated until [start, end] stabilizes.
-func (v *version) overlaps(
+func (v *Version) Overlaps(
 	level int, cmp Compare, start, end []byte,
-) (ret []fileMetadata) {
+) (ret []FileMetadata) {
 	if level == 0 {
 		// The sstables in level 0 can overlap with each other. As soon as we find
 		// one sstable that overlaps with our target range, we need to expand the
 		// range and find all sstables that overlap with the expanded range.
 	loop:
 		for {
-			for _, meta := range v.files[level] {
-				smallest := meta.smallest.UserKey
-				largest := meta.largest.UserKey
+			for _, meta := range v.Files[level] {
+				smallest := meta.Smallest.UserKey
+				largest := meta.Largest.UserKey
 				if cmp(largest, start) < 0 {
 					// meta is completely before the specified range; skip it.
 					continue
@@ -267,12 +300,12 @@ func (v *version) overlaps(
 
 	// Binary search to find the range of files which overlaps with our target
 	// range.
-	files := v.files[level]
+	files := v.Files[level]
 	lower := sort.Search(len(files), func(i int) bool {
-		return cmp(files[i].largest.UserKey, start) >= 0
+		return cmp(files[i].Largest.UserKey, start) >= 0
 	})
 	upper := sort.Search(len(files), func(i int) bool {
-		return cmp(files[i].smallest.UserKey, end) > 0
+		return cmp(files[i].Smallest.UserKey, end) > 0
 	})
 	if lower >= upper {
 		return nil
@@ -280,35 +313,35 @@ func (v *version) overlaps(
 	return files[lower:upper]
 }
 
-// checkOrdering checks that the files are consistent with respect to
+// CheckOrdering checks that the files are consistent with respect to
 // increasing file numbers (for level 0 files) and increasing and non-
 // overlapping internal key ranges (for level non-0 files).
-func (v *version) checkOrdering(cmp Compare) error {
-	for level, ff := range v.files {
+func (v *Version) CheckOrdering(cmp Compare) error {
+	for level, ff := range v.Files {
 		if level == 0 {
 			for i := 1; i < len(ff); i++ {
 				prev := &ff[i-1]
 				f := &ff[i]
-				if prev.largestSeqNum >= f.largestSeqNum {
+				if prev.LargestSeqNum >= f.LargestSeqNum {
 					return fmt.Errorf("level 0 files are not in increasing largest seqNum order: %d, %d",
-						prev.largestSeqNum, f.largestSeqNum)
+						prev.LargestSeqNum, f.LargestSeqNum)
 				}
-				if prev.smallestSeqNum >= f.smallestSeqNum {
+				if prev.SmallestSeqNum >= f.SmallestSeqNum {
 					return fmt.Errorf("level 0 files are not in increasing smallest seqNum order: %d, %d",
-						prev.smallestSeqNum, f.smallestSeqNum)
+						prev.SmallestSeqNum, f.SmallestSeqNum)
 				}
 			}
 		} else {
 			for i := 1; i < len(ff); i++ {
 				prev := &ff[i-1]
 				f := &ff[i]
-				if base.InternalCompare(cmp, prev.largest, f.smallest) >= 0 {
+				if base.InternalCompare(cmp, prev.Largest, f.Smallest) >= 0 {
 					return fmt.Errorf("level non-0 files are not in increasing ikey order: %s, %s\n%s",
-						prev.largest, f.smallest, v.DebugString())
+						prev.Largest, f.Smallest, v.DebugString())
 				}
-				if base.InternalCompare(cmp, f.smallest, f.largest) > 0 {
+				if base.InternalCompare(cmp, f.Smallest, f.Largest) > 0 {
 					return fmt.Errorf("level non-0 file has inconsistent bounds: %s, %s",
-						f.smallest, f.largest)
+						f.Smallest, f.Largest)
 				}
 			}
 		}
@@ -316,29 +349,40 @@ func (v *version) checkOrdering(cmp Compare) error {
 	return nil
 }
 
-type versionList struct {
+// VersionList holds a list of versions. The versions are ordered from oldest
+// to newest.
+type VersionList struct {
 	mu   *sync.Mutex
-	root version
+	root Version
 }
 
-func (l *versionList) init() {
+// Init initializes the version list.
+func (l *VersionList) Init(mu *sync.Mutex) {
+	l.mu = mu
 	l.root.next = &l.root
 	l.root.prev = &l.root
 }
 
-func (l *versionList) empty() bool {
+// Empty returns true if the list is empty, and false otherwise.
+func (l *VersionList) Empty() bool {
 	return l.root.next == &l.root
 }
 
-func (l *versionList) front() *version {
+// Front returns the oldest version in the list. Note that this version is only
+// valid if Empty() returns true.
+func (l *VersionList) Front() *Version {
 	return l.root.next
 }
 
-func (l *versionList) back() *version {
+// Back returns the newest version in the list. Note that this version is only
+// valid if Empty() returns true.
+func (l *VersionList) Back() *Version {
 	return l.root.prev
 }
 
-func (l *versionList) pushBack(v *version) {
+// PushBack adds a new version to the back of the list. This new version
+// becomes the "newest" version in the list.
+func (l *VersionList) PushBack(v *Version) {
 	if v.list != nil || v.prev != nil || v.next != nil {
 		panic("pebble: version list is inconsistent")
 	}
@@ -349,7 +393,8 @@ func (l *versionList) pushBack(v *version) {
 	v.list = l
 }
 
-func (l *versionList) remove(v *version) {
+// Remove removes the specified version from the list.
+func (l *VersionList) Remove(v *Version) {
 	if v == &l.root {
 		panic("pebble: cannot remove version list root node")
 	}
diff --git a/version_edit.go b/internal/manifest/version_edit.go
similarity index 65%
rename from version_edit.go
rename to internal/manifest/version_edit.go
index 377c8bc..9017805 100644
--- a/version_edit.go
+++ b/internal/manifest/version_edit.go
@@ -2,7 +2,7 @@
 // of this source code is governed by a BSD-style license that can be found in
 // the LICENSE file.
 
-package pebble
+package manifest
 
 import (
 	"bufio"
@@ -11,7 +11,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"sort"
 	"sync/atomic"
 
 	"github.com/petermattis/pebble/internal/base"
@@ -56,28 +55,34 @@ const (
 	customTagNonSafeIgnoreMask = 1 << 6
 )
 
-type deletedFileEntry struct {
-	level   int
-	fileNum uint64
+// DeletedFileEntry holds the state for a file deletion from a level. The file
+// itself might still be referenced by another level.
+type DeletedFileEntry struct {
+	Level   int
+	FileNum uint64
 }
 
-type newFileEntry struct {
-	level int
-	meta  fileMetadata
+// NewFileEntry holds the state for a new file or one moved from a different
+// level.
+type NewFileEntry struct {
+	Level int
+	Meta  FileMetadata
 }
 
-type versionEdit struct {
-	comparatorName string
-	logNumber      uint64
-	prevLogNumber  uint64
-	nextFileNumber uint64
-	lastSequence   uint64
-	deletedFiles   map[deletedFileEntry]bool // A set of deletedFileEntry values.
-	newFiles       []newFileEntry
-	metrics        map[int]*LevelMetrics // level -> metrics update
+// VersionEdit holds the state for an edit to a Version along with other
+// on-disk state (log numbers, next file number, and the last sequence number).
+type VersionEdit struct {
+	ComparerName string
+	LogNum       uint64
+	PrevLogNum   uint64
+	NextFileNum  uint64
+	LastSeqNum   uint64
+	DeletedFiles map[DeletedFileEntry]bool // set of DeletedFileEntry values
+	NewFiles     []NewFileEntry
 }
 
-func (v *versionEdit) decode(r io.Reader) error {
+// Decode decodes an edit from the specified reader.
+func (v *VersionEdit) Decode(r io.Reader) error {
 	br, ok := r.(byteReader)
 	if !ok {
 		br = bufio.NewReader(r)
@@ -97,28 +102,28 @@ func (v *versionEdit) decode(r io.Reader) error {
 			if err != nil {
 				return err
 			}
-			v.comparatorName = string(s)
+			v.ComparerName = string(s)
 
 		case tagLogNumber:
 			n, err := d.readUvarint()
 			if err != nil {
 				return err
 			}
-			v.logNumber = n
+			v.LogNum = n
 
 		case tagNextFileNumber:
 			n, err := d.readUvarint()
 			if err != nil {
 				return err
 			}
-			v.nextFileNumber = n
+			v.NextFileNum = n
 
 		case tagLastSequence:
 			n, err := d.readUvarint()
 			if err != nil {
 				return err
 			}
-			v.lastSequence = n
+			v.LastSeqNum = n
 
 		case tagCompactPointer:
 			if _, err := d.readLevel(); err != nil {
@@ -138,10 +143,10 @@ func (v *versionEdit) decode(r io.Reader) error {
 			if err != nil {
 				return err
 			}
-			if v.deletedFiles == nil {
-				v.deletedFiles = make(map[deletedFileEntry]bool)
+			if v.DeletedFiles == nil {
+				v.DeletedFiles = make(map[DeletedFileEntry]bool)
 			}
-			v.deletedFiles[deletedFileEntry{level, fileNum}] = true
+			v.DeletedFiles[DeletedFileEntry{level, fileNum}] = true
 
 		case tagNewFile, tagNewFile2, tagNewFile3, tagNewFile4:
 			level, err := d.readLevel()
@@ -214,16 +219,16 @@ func (v *versionEdit) decode(r io.Reader) error {
 					}
 				}
 			}
-			v.newFiles = append(v.newFiles, newFileEntry{
-				level: level,
-				meta: fileMetadata{
-					fileNum:             fileNum,
-					size:                size,
-					smallest:            base.DecodeInternalKey(smallest),
-					largest:             base.DecodeInternalKey(largest),
-					smallestSeqNum:      smallestSeqNum,
-					largestSeqNum:       largestSeqNum,
-					markedForCompaction: markedForCompaction,
+			v.NewFiles = append(v.NewFiles, NewFileEntry{
+				Level: level,
+				Meta: FileMetadata{
+					FileNum:             fileNum,
+					Size:                size,
+					Smallest:            base.DecodeInternalKey(smallest),
+					Largest:             base.DecodeInternalKey(largest),
+					SmallestSeqNum:      smallestSeqNum,
+					LargestSeqNum:       largestSeqNum,
+					MarkedForCompaction: markedForCompaction,
 				},
 			})
 
@@ -232,7 +237,7 @@ func (v *versionEdit) decode(r io.Reader) error {
 			if err != nil {
 				return err
 			}
-			v.prevLogNumber = n
+			v.PrevLogNum = n
 
 		case tagColumnFamily, tagColumnFamilyAdd, tagColumnFamilyDrop, tagMaxColumnFamily:
 			return fmt.Errorf("column families are not supported")
@@ -244,50 +249,51 @@ func (v *versionEdit) decode(r io.Reader) error {
 	return nil
 }
 
-func (v *versionEdit) encode(w io.Writer) error {
+// Encode encodes an edit to the specified writer.
+func (v *VersionEdit) Encode(w io.Writer) error {
 	e := versionEditEncoder{new(bytes.Buffer)}
-	if v.comparatorName != "" {
+	if v.ComparerName != "" {
 		e.writeUvarint(tagComparator)
-		e.writeString(v.comparatorName)
+		e.writeString(v.ComparerName)
 	}
-	if v.logNumber != 0 {
+	if v.LogNum != 0 {
 		e.writeUvarint(tagLogNumber)
-		e.writeUvarint(v.logNumber)
+		e.writeUvarint(v.LogNum)
 	}
-	if v.prevLogNumber != 0 {
+	if v.PrevLogNum != 0 {
 		e.writeUvarint(tagPrevLogNumber)
-		e.writeUvarint(v.prevLogNumber)
+		e.writeUvarint(v.PrevLogNum)
 	}
-	if v.nextFileNumber != 0 {
+	if v.NextFileNum != 0 {
 		e.writeUvarint(tagNextFileNumber)
-		e.writeUvarint(v.nextFileNumber)
+		e.writeUvarint(v.NextFileNum)
 	}
-	if v.lastSequence != 0 {
+	if v.LastSeqNum != 0 {
 		e.writeUvarint(tagLastSequence)
-		e.writeUvarint(v.lastSequence)
+		e.writeUvarint(v.LastSeqNum)
 	}
-	for x := range v.deletedFiles {
+	for x := range v.DeletedFiles {
 		e.writeUvarint(tagDeletedFile)
-		e.writeUvarint(uint64(x.level))
-		e.writeUvarint(x.fileNum)
+		e.writeUvarint(uint64(x.Level))
+		e.writeUvarint(x.FileNum)
 	}
-	for _, x := range v.newFiles {
+	for _, x := range v.NewFiles {
 		var customFields bool
-		if x.meta.markedForCompaction {
+		if x.Meta.MarkedForCompaction {
 			customFields = true
 			e.writeUvarint(tagNewFile4)
 		} else {
 			e.writeUvarint(tagNewFile2)
 		}
-		e.writeUvarint(uint64(x.level))
-		e.writeUvarint(x.meta.fileNum)
-		e.writeUvarint(x.meta.size)
-		e.writeKey(x.meta.smallest)
-		e.writeKey(x.meta.largest)
-		e.writeUvarint(x.meta.smallestSeqNum)
-		e.writeUvarint(x.meta.largestSeqNum)
+		e.writeUvarint(uint64(x.Level))
+		e.writeUvarint(x.Meta.FileNum)
+		e.writeUvarint(x.Meta.Size)
+		e.writeKey(x.Meta.Smallest)
+		e.writeKey(x.Meta.Largest)
+		e.writeUvarint(x.Meta.SmallestSeqNum)
+		e.writeUvarint(x.Meta.LargestSeqNum)
 		if customFields {
-			if x.meta.markedForCompaction {
+			if x.Meta.MarkedForCompaction {
 				e.writeUvarint(customTagNeedsCompaction)
 				e.writeBytes([]byte{1})
 			}
@@ -323,7 +329,7 @@ func (d versionEditDecoder) readLevel() (int, error) {
 	if err != nil {
 		return 0, err
 	}
-	if u >= numLevels {
+	if u >= NumLevels {
 		return 0, errCorruptManifest
 	}
 	return int(u), nil
@@ -367,49 +373,49 @@ func (e versionEditEncoder) writeUvarint(u uint64) {
 	e.Write(buf[:n])
 }
 
-// bulkVersionEdit summarizes the files added and deleted from a set of version
+// BulkVersionEdit summarizes the files added and deleted from a set of version
 // edits.
-//
-// The C++ LevelDB code calls this concept a VersionSet::Builder.
-type bulkVersionEdit struct {
-	added   [numLevels][]fileMetadata
-	deleted [numLevels]map[uint64]bool // map[uint64]bool is a set of fileNums.
+type BulkVersionEdit struct {
+	Added   [NumLevels][]FileMetadata
+	Deleted [NumLevels]map[uint64]bool // map[uint64]bool is a set of fileNums
 }
 
-func (b *bulkVersionEdit) accumulate(ve *versionEdit) {
-	for df := range ve.deletedFiles {
-		dmap := b.deleted[df.level]
+// Accumulate adds the file addition and deletions in the specified version
+// edit to the bulk edit's internal state.
+func (b *BulkVersionEdit) Accumulate(ve *VersionEdit) {
+	for df := range ve.DeletedFiles {
+		dmap := b.Deleted[df.Level]
 		if dmap == nil {
 			dmap = make(map[uint64]bool)
-			b.deleted[df.level] = dmap
+			b.Deleted[df.Level] = dmap
 		}
-		dmap[df.fileNum] = true
+		dmap[df.FileNum] = true
 	}
 
-	for _, nf := range ve.newFiles {
-		if dmap := b.deleted[nf.level]; dmap != nil {
-			delete(dmap, nf.meta.fileNum)
+	for _, nf := range ve.NewFiles {
+		if dmap := b.Deleted[nf.Level]; dmap != nil {
+			delete(dmap, nf.Meta.FileNum)
 		}
-		b.added[nf.level] = append(b.added[nf.level], nf.meta)
+		b.Added[nf.Level] = append(b.Added[nf.Level], nf.Meta)
 	}
 }
 
-// apply applies the delta b to a base version to produce a new version. The
+// Apply applies the delta b to a base version to produce a new version. The
 // new version is consistent with respect to the internal key comparer icmp.
 //
 // base may be nil, which is equivalent to a pointer to a zero version.
-func (b *bulkVersionEdit) apply(
-	opts *Options, base *version, cmp Compare,
-) (*version, error) {
-	v := new(version)
-	for level := range v.files {
-		if len(b.added[level]) == 0 && len(b.deleted[level]) == 0 {
+func (b *BulkVersionEdit) Apply(
+	opts *Options, base *Version, cmp Compare,
+) (*Version, error) {
+	v := new(Version)
+	for level := range v.Files {
+		if len(b.Added[level]) == 0 && len(b.Deleted[level]) == 0 {
 			// There are no edits on this level.
 			if base == nil {
 				continue
 			}
-			files := base.files[level]
-			v.files[level] = files
+			files := base.Files[level]
+			v.Files[level] = files
 			// We still have to bump the ref count for all files.
 			for i := range files {
 				atomic.AddInt32(files[i].refs, 1)
@@ -417,30 +423,30 @@ func (b *bulkVersionEdit) apply(
 			continue
 		}
 
-		combined := [2][]fileMetadata{
+		combined := [2][]FileMetadata{
 			nil,
-			b.added[level],
+			b.Added[level],
 		}
 		if base != nil {
-			combined[0] = base.files[level]
+			combined[0] = base.Files[level]
 		}
 		n := len(combined[0]) + len(combined[1])
 		if n == 0 {
 			continue
 		}
-		v.files[level] = make([]fileMetadata, 0, n)
-		dmap := b.deleted[level]
+		v.Files[level] = make([]FileMetadata, 0, n)
+		dmap := b.Deleted[level]
 
 		for _, ff := range combined {
 			for _, f := range ff {
-				if dmap != nil && dmap[f.fileNum] {
+				if dmap != nil && dmap[f.FileNum] {
 					continue
 				}
 				if f.refs == nil {
 					f.refs = new(int32)
 				}
 				atomic.AddInt32(f.refs, 1)
-				v.files[level] = append(v.files[level], f)
+				v.Files[level] = append(v.Files[level], f)
 			}
 		}
 
@@ -449,12 +455,12 @@ func (b *bulkVersionEdit) apply(
 		// efficient to sort b.addFiles[level] and then merge the two sorted
 		// slices.
 		if level == 0 {
-			sort.Sort(bySeqNum(v.files[level]))
+			SortBySeqNum(v.Files[level])
 		} else {
-			sort.Sort(bySmallest{v.files[level], cmp})
+			SortBySmallest(v.Files[level], cmp)
 		}
 	}
-	if err := v.checkOrdering(cmp); err != nil {
+	if err := v.CheckOrdering(cmp); err != nil {
 		return nil, fmt.Errorf("pebble: internal error: %v", err)
 	}
 	return v, nil
diff --git a/level_iter.go b/level_iter.go
index 0b66e64..a1fdad9 100644
--- a/level_iter.go
+++ b/level_iter.go
@@ -108,7 +108,7 @@ func (l *levelIter) findFileGE(key []byte) int {
 	//
 	// TODO(peter): inline the binary search.
 	return sort.Search(len(l.files), func(i int) bool {
-		largest := &l.files[i].largest
+		largest := &l.files[i].Largest
 		c := l.cmp(largest.UserKey, key)
 		if c > 0 {
 			return true
@@ -120,7 +120,7 @@ func (l *levelIter) findFileGE(key []byte) int {
 func (l *levelIter) findFileLT(key []byte) int {
 	// Find the last file whose smallest key is < ikey.
 	index := sort.Search(len(l.files), func(i int) bool {
-		return l.cmp(l.files[i].smallest.UserKey, key) >= 0
+		return l.cmp(l.files[i].Smallest.UserKey, key) >= 0
 	})
 	return index - 1
 }
@@ -150,14 +150,14 @@ func (l *levelIter) loadFile(index, dir int) bool {
 		f := &l.files[l.index]
 		l.tableOpts.LowerBound = l.opts.GetLowerBound()
 		if l.tableOpts.LowerBound != nil {
-			if l.cmp(f.largest.UserKey, l.tableOpts.LowerBound) < 0 {
+			if l.cmp(f.Largest.UserKey, l.tableOpts.LowerBound) < 0 {
 				// The largest key in the sstable is smaller than the lower bound.
 				if dir < 0 {
 					return false
 				}
 				continue
 			}
-			if l.cmp(l.tableOpts.LowerBound, f.smallest.UserKey) < 0 {
+			if l.cmp(l.tableOpts.LowerBound, f.Smallest.UserKey) < 0 {
 				// The lower bound is smaller than the smallest key in the
 				// table. Iteration within the table does not need to check the lower
 				// bound.
@@ -166,7 +166,7 @@ func (l *levelIter) loadFile(index, dir int) bool {
 		}
 		l.tableOpts.UpperBound = l.opts.GetUpperBound()
 		if l.tableOpts.UpperBound != nil {
-			if l.cmp(f.smallest.UserKey, l.tableOpts.UpperBound) >= 0 {
+			if l.cmp(f.Smallest.UserKey, l.tableOpts.UpperBound) >= 0 {
 				// The smallest key in the sstable is greater than or equal to the
 				// lower bound.
 				if dir > 0 {
@@ -174,7 +174,7 @@ func (l *levelIter) loadFile(index, dir int) bool {
 				}
 				continue
 			}
-			if l.cmp(l.tableOpts.UpperBound, f.largest.UserKey) > 0 {
+			if l.cmp(l.tableOpts.UpperBound, f.Largest.UserKey) > 0 {
 				// The upper bound is greater than the largest key in the
 				// table. Iteration within the table does not need to check the upper
 				// bound.
@@ -191,7 +191,7 @@ func (l *levelIter) loadFile(index, dir int) bool {
 			*l.rangeDelIter = rangeDelIter
 		}
 		if l.largestUserKey != nil {
-			*l.largestUserKey = f.largest.UserKey
+			*l.largestUserKey = f.Largest.UserKey
 		}
 		return true
 	}
@@ -334,8 +334,8 @@ func (l *levelIter) skipEmptyFileForward() (*InternalKey, []byte) {
 			// We're being used as part of an Iterator and we've reached the end of
 			// the sstable. If the boundary is a range deletion tombstone, return
 			// that key.
-			if f := &l.files[l.index]; f.largest.Kind() == InternalKeyKindRangeDelete {
-				l.boundary = &f.largest
+			if f := &l.files[l.index]; f.Largest.Kind() == InternalKeyKindRangeDelete {
+				l.boundary = &f.Largest
 				return l.boundary, nil
 			}
 			*l.rangeDelIter = nil
@@ -362,8 +362,8 @@ func (l *levelIter) skipEmptyFileBackward() (*InternalKey, []byte) {
 			// We're being used as part of an Iterator and we've reached the end of
 			// the sstable. If the boundary is a range deletion tombstone, return
 			// that key.
-			if f := &l.files[l.index]; f.smallest.Kind() == InternalKeyKindRangeDelete {
-				l.boundary = &f.smallest
+			if f := &l.files[l.index]; f.Smallest.Kind() == InternalKeyKindRangeDelete {
+				l.boundary = &f.Smallest
 				return l.boundary, nil
 			}
 			*l.rangeDelIter = nil
diff --git a/metrics.go b/metrics.go
index 2b27992..c4b971d 100644
--- a/metrics.go
+++ b/metrics.go
@@ -53,6 +53,8 @@ func (m *LevelMetrics) WriteAmp() float64 {
 	return float64(m.BytesWritten) / float64(m.BytesIn)
 }
 
+// format generates a string of the receiver's metrics, formatting it into the
+// supplied buffer.
 func (m *LevelMetrics) format(buf *bytes.Buffer) {
 	fmt.Fprintf(buf, "%6d %7s %7.2f %7s %7s %7s %7s %7s %7.1f\n",
 		m.NumFiles,
diff --git a/open.go b/open.go
index 874ee01..32785a8 100644
--- a/open.go
+++ b/open.go
@@ -37,8 +37,8 @@ func allocDBNum() uint64 {
 func createDB(dirname string, opts *Options) (retErr error) {
 	const manifestFileNum = 1
 	ve := versionEdit{
-		comparatorName: opts.Comparer.Name,
-		nextFileNumber: manifestFileNum + 1,
+		ComparerName: opts.Comparer.Name,
+		NextFileNum:  manifestFileNum + 1,
 	}
 	manifestFilename := base.MakeFilename(dirname, fileTypeManifest, manifestFileNum)
 	f, err := opts.FS.Create(manifestFilename)
@@ -57,7 +57,7 @@ func createDB(dirname string, opts *Options) (retErr error) {
 	if err != nil {
 		return err
 	}
-	err = ve.encode(w)
+	err = ve.Encode(w)
 	if err != nil {
 		return err
 	}
@@ -197,7 +197,7 @@ func Open(dirname string, opts *Options) (*DB, error) {
 		}
 		switch ft {
 		case fileTypeLog:
-			if fn >= d.mu.versions.logNumber || fn == d.mu.versions.prevLogNumber {
+			if fn >= d.mu.versions.logNum || fn == d.mu.versions.prevLogNum {
 				logFiles = append(logFiles, fileNumAndName{fn, filename})
 			}
 		case fileTypeOptions:
@@ -228,9 +228,9 @@ func Open(dirname string, opts *Options) (*DB, error) {
 
 	if !d.opts.ReadOnly {
 		// Create an empty .log file.
-		ve.logNumber = d.mu.versions.nextFileNum()
-		d.mu.log.queue = append(d.mu.log.queue, ve.logNumber)
-		logFile, err := opts.FS.Create(base.MakeFilename(d.walDirname, fileTypeLog, ve.logNumber))
+		ve.LogNum = d.mu.versions.getNextFileNum()
+		d.mu.log.queue = append(d.mu.log.queue, ve.LogNum)
+		logFile, err := opts.FS.Create(base.MakeFilename(d.walDirname, fileTypeLog, ve.LogNum))
 		if err != nil {
 			return nil, err
 		}
@@ -241,11 +241,11 @@ func Open(dirname string, opts *Options) (*DB, error) {
 			BytesPerSync:    d.opts.BytesPerSync,
 			PreallocateSize: d.walPreallocateSize(),
 		})
-		d.mu.log.LogWriter = record.NewLogWriter(logFile, ve.logNumber)
+		d.mu.log.LogWriter = record.NewLogWriter(logFile, ve.LogNum)
 		d.mu.versions.metrics.WAL.Files++
 
 		// Write a new manifest to disk.
-		if err := d.mu.versions.logAndApply(0, &ve, d.dataDir); err != nil {
+		if err := d.mu.versions.logAndApply(0, &ve, nil, d.dataDir); err != nil {
 			return nil, err
 		}
 	}
@@ -253,7 +253,7 @@ func Open(dirname string, opts *Options) (*DB, error) {
 
 	if !d.opts.ReadOnly {
 		// Write the current options to disk.
-		d.optionsFileNum = d.mu.versions.nextFileNum()
+		d.optionsFileNum = d.mu.versions.getNextFileNum()
 		optionsFile, err := opts.FS.Create(base.MakeFilename(dirname, fileTypeOptions, d.optionsFileNum))
 		if err != nil {
 			return nil, err
@@ -364,7 +364,7 @@ func (d *DB) replayWAL(
 		if err != nil {
 			return 0, err
 		}
-		ve.newFiles = append(ve.newFiles, newVE.newFiles...)
+		ve.NewFiles = append(ve.NewFiles, newVE.NewFiles...)
 		// Strictly speaking, it's too early to delete from d.pendingOutputs, but
 		// we are replaying the log file, which happens before Open returns, so
 		// there is no possibility of deleteObsoleteFiles being called concurrently
diff --git a/read_state.go b/read_state.go
index e507a3b..0d80fc5 100644
--- a/read_state.go
+++ b/read_state.go
@@ -37,7 +37,7 @@ func (s *readState) ref() {
 // is held by the caller.
 func (s *readState) unref() {
 	if atomic.AddInt32(&s.refcnt, -1) == 0 {
-		s.current.unref()
+		s.current.Unref()
 	}
 }
 
@@ -47,7 +47,7 @@ func (s *readState) unref() {
 // unref() if DB.mu is NOT held by the caller.
 func (s *readState) unrefLocked() {
 	if atomic.AddInt32(&s.refcnt, -1) == 0 {
-		s.current.unrefLocked()
+		s.current.UnrefLocked()
 	}
 }
 
@@ -69,7 +69,7 @@ func (d *DB) updateReadStateLocked() {
 		current:   d.mu.versions.currentVersion(),
 		memtables: d.mu.mem.queue,
 	}
-	s.current.ref()
+	s.current.Ref()
 
 	d.readState.Lock()
 	old := d.readState.val
diff --git a/table_cache.go b/table_cache.go
index 9bd4f1b..5a163ea 100644
--- a/table_cache.go
+++ b/table_cache.go
@@ -42,7 +42,7 @@ func (c *tableCache) getShard(fileNum uint64) *tableCacheShard {
 func (c *tableCache) newIters(
 	meta *fileMetadata, opts *IterOptions, bytesIterated *uint64,
 ) (internalIterator, internalIterator, error) {
-	return c.getShard(meta.fileNum).newIters(meta, opts, bytesIterated)
+	return c.getShard(meta.FileNum).newIters(meta, opts, bytesIterated)
 }
 
 func (c *tableCache) evict(fileNum uint64) {
@@ -152,7 +152,7 @@ func (c *tableCacheShard) newIters(
 //
 // c.mu must be held when calling this.
 func (c *tableCacheShard) releaseNode(n *tableCacheNode) {
-	delete(c.mu.nodes, n.meta.fileNum)
+	delete(c.mu.nodes, n.meta.FileNum)
 	n.next.prev = n.prev
 	n.prev.next = n.next
 	n.prev = nil
@@ -180,7 +180,7 @@ func (c *tableCacheShard) findNode(meta *fileMetadata) *tableCacheNode {
 	// Fast-path for a hit in the cache. We grab the lock in shared mode, and use
 	// a batching mechanism to perform updates to the LRU list.
 	c.mu.RLock()
-	if n := c.mu.nodes[meta.fileNum]; n != nil {
+	if n := c.mu.nodes[meta.FileNum]; n != nil {
 		// The caller is responsible for decrementing the refCount.
 		atomic.AddInt32(&n.refCount, 1)
 		c.mu.RUnlock()
@@ -209,7 +209,7 @@ func (c *tableCacheShard) findNode(meta *fileMetadata) *tableCacheNode {
 		c.hitsPool.Put(hits)
 	}
 
-	n := c.mu.nodes[meta.fileNum]
+	n := c.mu.nodes[meta.FileNum]
 	if n == nil {
 		n = &tableCacheNode{
 			// Cache the closure invoked when an iterator is closed. This avoids an
@@ -228,7 +228,7 @@ func (c *tableCacheShard) findNode(meta *fileMetadata) *tableCacheNode {
 			refCount: 1,
 			loaded:   make(chan struct{}),
 		}
-		c.mu.nodes[meta.fileNum] = n
+		c.mu.nodes[meta.FileNum] = n
 		if len(c.mu.nodes) > c.size {
 			// Release the tail node.
 			c.releaseNode(c.mu.lru.prev)
@@ -328,16 +328,16 @@ type tableCacheNode struct {
 
 func (n *tableCacheNode) load(c *tableCacheShard) {
 	// Try opening the fileTypeTable first.
-	f, err := c.fs.Open(base.MakeFilename(c.dirname, fileTypeTable, n.meta.fileNum),
+	f, err := c.fs.Open(base.MakeFilename(c.dirname, fileTypeTable, n.meta.FileNum),
 		vfs.RandomReadsOption)
 	if err != nil {
 		n.err = err
 		close(n.loaded)
 		return
 	}
-	n.reader, n.err = sstable.NewReader(f, c.dbNum, n.meta.fileNum, c.opts)
-	if n.meta.smallestSeqNum == n.meta.largestSeqNum {
-		n.reader.Properties.GlobalSeqNum = n.meta.largestSeqNum
+	n.reader, n.err = sstable.NewReader(f, c.dbNum, n.meta.FileNum, c.opts)
+	if n.meta.SmallestSeqNum == n.meta.LargestSeqNum {
+		n.reader.Properties.GlobalSeqNum = n.meta.LargestSeqNum
 	}
 	close(n.loaded)
 }
diff --git a/tool/manifest.go b/tool/manifest.go
index 7a1ea71..17df667 100644
--- a/tool/manifest.go
+++ b/tool/manifest.go
@@ -8,6 +8,10 @@ import (
 	"fmt"
 
 	"github.com/petermattis/pebble/internal/base"
+	"github.com/petermattis/pebble/internal/manifest"
+	"github.com/petermattis/pebble/internal/record"
+	"github.com/petermattis/pebble/sstable"
+	"github.com/petermattis/pebble/vfs"
 	"github.com/spf13/cobra"
 )
 
@@ -16,10 +20,18 @@ import (
 type manifestT struct {
 	Root *cobra.Command
 	Dump *cobra.Command
+
+	opts      *sstable.Options
+	comparers sstable.Comparers
+	fmtKey    formatter
 }
 
-func newManifest(opts *base.Options) *manifestT {
-	m := &manifestT{}
+func newManifest(opts *base.Options, comparers sstable.Comparers) *manifestT {
+	m := &manifestT{
+		opts:      opts,
+		comparers: comparers,
+	}
+	m.fmtKey.mustSet("quoted")
 
 	m.Root = &cobra.Command{
 		Use:   "manifest",
@@ -36,10 +48,101 @@ Print the contents of the MANIFEST files.
 	}
 
 	m.Root.AddCommand(m.Dump)
+
+	m.Dump.Flags().Var(
+		&m.fmtKey, "key", "key formatter")
 	return m
 }
 
 func (m *manifestT) runDump(cmd *cobra.Command, args []string) {
-	fmt.Fprintf(stderr, "TODO(peter): \"manifest dump\" unimplemented\n")
-	osExit(1)
+	for _, arg := range args {
+		func() {
+			f, err := vfs.Default.Open(arg)
+			if err != nil {
+				fmt.Fprintf(stderr, "%s\n", err)
+				return
+			}
+			defer f.Close()
+
+			fmt.Fprintf(stdout, "%s\n", arg)
+
+			var bve manifest.BulkVersionEdit
+			var cmp *base.Comparer
+			rr := record.NewReader(f, 0 /* logNum */)
+			for {
+				offset := rr.Offset()
+				r, err := rr.Next()
+				if err != nil {
+					fmt.Fprintf(stdout, "%s\n", err)
+					break
+				}
+
+				var ve manifest.VersionEdit
+				err = ve.Decode(r)
+				if err != nil {
+					fmt.Fprintf(stdout, "%s\n", err)
+					break
+				}
+				bve.Accumulate(&ve)
+
+				empty := true
+				fmt.Fprintf(stdout, "%d\n", offset)
+				if ve.ComparerName != "" {
+					empty = false
+					fmt.Fprintf(stdout, "  comparer:     %s", ve.ComparerName)
+					cmp = m.comparers[ve.ComparerName]
+					if cmp == nil {
+						fmt.Fprintf(stdout, " (unknown)")
+					}
+					fmt.Fprintf(stdout, "\n")
+				}
+				if ve.LogNum != 0 {
+					empty = false
+					fmt.Fprintf(stdout, "  log-num:      %d\n", ve.LogNum)
+				}
+				if ve.PrevLogNum != 0 {
+					empty = false
+					fmt.Fprintf(stdout, "  prev-log-num: %d\n", ve.PrevLogNum)
+				}
+				if ve.LastSeqNum != 0 {
+					empty = false
+					fmt.Fprintf(stdout, "  last-seq-num: %d\n", ve.LastSeqNum)
+				}
+				for df := range ve.DeletedFiles {
+					empty = false
+					fmt.Fprintf(stdout, "  deleted:      L%d %d\n", df.Level, df.FileNum)
+				}
+				for _, nf := range ve.NewFiles {
+					empty = false
+					fmt.Fprintf(stdout, "  added:        L%d %d:%d",
+						nf.Level, nf.Meta.FileNum, nf.Meta.Size)
+					formatKeyRange(stdout, m.fmtKey, &nf.Meta.Smallest, &nf.Meta.Largest)
+					fmt.Fprintf(stdout, "\n")
+				}
+				if empty {
+					// NB: An empty version edit can happen if we log a version edit with
+					// a zero field. RocksDB does this with a version edit that contains
+					// `LogNum == 0`.
+					fmt.Fprintf(stdout, "  <empty>\n")
+				}
+			}
+
+			if cmp != nil {
+				v, err := bve.Apply(m.opts, nil, cmp.Compare)
+				if err != nil {
+					fmt.Fprintf(stdout, "%s\n", err)
+					return
+				}
+				for level := range v.Files {
+					fmt.Fprintf(stdout, "--- L%d ---\n", level)
+					for j := range v.Files[level] {
+						f := &v.Files[level][j]
+						fmt.Fprintf(stdout, "  %d:%d", f.FileNum, f.Size)
+						formatKeyRange(stdout, m.fmtKey, &f.Smallest, &f.Largest)
+						fmt.Fprintf(stdout, "\n")
+					}
+				}
+			}
+		}()
+	}
 }
diff --git a/tool/tool.go b/tool/tool.go
index b9ee812..a2c63eb 100644
--- a/tool/tool.go
+++ b/tool/tool.go
@@ -43,7 +43,7 @@ func New() *T {
 	t.RegisterMerger(base.DefaultMerger)
 
 	t.db = newDB(&t.opts)
-	t.manifest = newManifest(&t.opts)
+	t.manifest = newManifest(&t.opts, t.comparers)
 	t.sstable = newSSTable(&t.opts, t.comparers, t.mergers)
 	t.wal = newWAL(&t.opts)
 	t.Commands = []*cobra.Command{
diff --git a/tool/util.go b/tool/util.go
index b104731..ba132df 100644
--- a/tool/util.go
+++ b/tool/util.go
@@ -100,15 +100,30 @@ func formatSize(w io.Writer, v []byte) {
 	fmt.Fprintf(w, "<%d>", len(v))
 }
 
+func formatKey(w io.Writer, fmtKey formatter, key *base.InternalKey) bool {
+	if fmtKey.spec == "null" {
+		return false
+	}
+	fmtKey.fn(w, key.UserKey)
+	fmt.Fprintf(w, "#%d,%d", key.SeqNum(), key.Kind())
+	return true
+}
+
+func formatKeyRange(w io.Writer, fmtKey formatter, start, end *base.InternalKey) {
+	if fmtKey.spec == "null" {
+		return
+	}
+	w.Write([]byte{'['})
+	formatKey(w, fmtKey, start)
+	w.Write([]byte{'-'})
+	formatKey(w, fmtKey, end)
+	w.Write([]byte{']'})
+}
+
 func formatKeyValue(
 	w io.Writer, fmtKey formatter, fmtValue formatter, key *base.InternalKey, value []byte,
 ) {
-	needDelimiter := false
-	if fmtKey.spec != "null" {
-		fmtKey.fn(w, key.UserKey)
-		fmt.Fprintf(w, "#%d,%d", key.SeqNum(), key.Kind())
-		needDelimiter = true
-	}
+	needDelimiter := formatKey(w, fmtKey, key)
 	if fmtValue.spec != "null" {
 		if needDelimiter {
 			w.Write([]byte{' '})
diff --git a/version_set.go b/version_set.go
index c8c3b1e..8b95ff9 100644
--- a/version_set.go
+++ b/version_set.go
@@ -12,10 +12,22 @@ import (
 	"sync/atomic"
 
 	"github.com/petermattis/pebble/internal/base"
+	"github.com/petermattis/pebble/internal/manifest"
 	"github.com/petermattis/pebble/internal/record"
 	"github.com/petermattis/pebble/vfs"
 )
 
+const numLevels = manifest.NumLevels
+
+// Provide type aliases for the various manifest structs.
+type bulkVersionEdit = manifest.BulkVersionEdit
+type deletedFileEntry = manifest.DeletedFileEntry
+type fileMetadata = manifest.FileMetadata
+type newFileEntry = manifest.NewFileEntry
+type version = manifest.Version
+type versionEdit = manifest.VersionEdit
+type versionList = manifest.VersionList
+
 // versionSet manages a collection of immutable versions, and manages the
 // creation of a new version from the most recent version. A new versions is
 // created from an existing version by applying a version edit which is just
@@ -39,16 +51,19 @@ type versionSet struct {
 
 	metrics VersionMetrics
 
+	// A pointer to versionSet.addObsoleteLocked. Avoids allocating a new closure
+	// on the creation of every version.
+	obsoleteFn        func(obsolete []uint64)
 	obsoleteTables    []uint64
 	obsoleteManifests []uint64
 	obsoleteOptions   []uint64
 
-	logNumber          uint64
-	prevLogNumber      uint64
-	nextFileNumber     uint64
-	logSeqNum          uint64 // next seqNum to use for WAL writes
-	visibleSeqNum      uint64 // visible seqNum (<= logSeqNum)
-	manifestFileNumber uint64
+	logNum          uint64
+	prevLogNum      uint64
+	nextFileNum     uint64
+	logSeqNum       uint64 // next seqNum to use for WAL writes
+	visibleSeqNum   uint64 // visible seqNum (<= logSeqNum)
+	manifestFileNum uint64
 
 	manifestFile vfs.File
 	manifest     *record.Writer
@@ -61,16 +76,16 @@ type versionSet struct {
 func (vs *versionSet) load(dirname string, opts *Options, mu *sync.Mutex) error {
 	vs.dirname = dirname
 	vs.mu = mu
-	vs.versions.mu = mu
 	vs.writerCond.L = mu
 	vs.opts = opts
 	vs.fs = opts.FS
 	vs.cmp = opts.Comparer.Compare
 	vs.cmpName = opts.Comparer.Name
 	vs.dynamicBaseLevel = true
-	vs.versions.init()
+	vs.versions.Init(mu)
+	vs.obsoleteFn = vs.addObsoleteLocked
 	// For historical reasons, the next file number is initialized to 2.
-	vs.nextFileNumber = 2
+	vs.nextFileNum = 2
 
 	// Read the CURRENT file to find the current manifest file.
 	current, err := vs.fs.Open(base.MakeFilename(dirname, fileTypeCurrent, 0))
@@ -116,42 +131,42 @@ func (vs *versionSet) load(dirname string, opts *Options, mu *sync.Mutex) error
 			return err
 		}
 		var ve versionEdit
-		err = ve.decode(r)
+		err = ve.Decode(r)
 		if err != nil {
 			return err
 		}
-		if ve.comparatorName != "" {
-			if ve.comparatorName != vs.cmpName {
+		if ve.ComparerName != "" {
+			if ve.ComparerName != vs.cmpName {
 				return fmt.Errorf("pebble: manifest file %q for DB %q: "+
 					"comparer name from file %q != comparer name from Options %q",
-					b, dirname, ve.comparatorName, vs.cmpName)
+					b, dirname, ve.ComparerName, vs.cmpName)
 			}
 		}
-		bve.accumulate(&ve)
-		if ve.logNumber != 0 {
-			vs.logNumber = ve.logNumber
+		bve.Accumulate(&ve)
+		if ve.LogNum != 0 {
+			vs.logNum = ve.LogNum
 		}
-		if ve.prevLogNumber != 0 {
-			vs.prevLogNumber = ve.prevLogNumber
+		if ve.PrevLogNum != 0 {
+			vs.prevLogNum = ve.PrevLogNum
 		}
-		if ve.nextFileNumber != 0 {
-			vs.nextFileNumber = ve.nextFileNumber
+		if ve.NextFileNum != 0 {
+			vs.nextFileNum = ve.NextFileNum
 		}
-		if ve.lastSequence != 0 {
-			vs.logSeqNum = ve.lastSequence
+		if ve.LastSeqNum != 0 {
+			vs.logSeqNum = ve.LastSeqNum
 		}
 	}
-	if vs.logNumber == 0 || vs.nextFileNumber == 0 {
-		if vs.nextFileNumber == 2 {
+	if vs.logNum == 0 || vs.nextFileNum == 0 {
+		if vs.nextFileNum == 2 {
 			// We have a freshly created DB.
 		} else {
 			return fmt.Errorf("pebble: incomplete manifest file %q for DB %q", b, dirname)
 		}
 	}
-	vs.markFileNumUsed(vs.logNumber)
-	vs.markFileNumUsed(vs.prevLogNumber)
+	vs.markFileNumUsed(vs.logNum)
+	vs.markFileNumUsed(vs.prevLogNum)
 
-	newVersion, err := bve.apply(opts, nil, vs.cmp)
+	newVersion, err := bve.Apply(opts, nil, vs.cmp)
 	if err != nil {
 		return err
 	}
@@ -163,7 +178,12 @@ func (vs *versionSet) load(dirname string, opts *Options, mu *sync.Mutex) error
 // to the current version, and installs the new version. DB.mu must be held
 // when calling this method and will be released temporarily while performing
 // file I/O.
-func (vs *versionSet) logAndApply(jobID int, ve *versionEdit, dir vfs.File) error {
+func (vs *versionSet) logAndApply(
+	jobID int,
+	ve *versionEdit,
+	metrics map[int]*LevelMetrics,
+	dir vfs.File,
+) error {
 	// Wait for any existing writing to the manifest to complete, then mark the
 	// manifest as busy.
 	for vs.writing {
@@ -175,21 +195,21 @@ func (vs *versionSet) logAndApply(jobID int, ve *versionEdit, dir vfs.File) erro
 		vs.writerCond.Signal()
 	}()
 
-	if ve.logNumber != 0 {
-		if ve.logNumber < vs.logNumber || vs.nextFileNumber <= ve.logNumber {
-			panic(fmt.Sprintf("pebble: inconsistent versionEdit logNumber %d", ve.logNumber))
+	if ve.LogNum != 0 {
+		if ve.LogNum < vs.logNum || vs.nextFileNum <= ve.LogNum {
+			panic(fmt.Sprintf("pebble: inconsistent versionEdit logNumber %d", ve.LogNum))
 		}
 	}
-	ve.nextFileNumber = vs.nextFileNumber
-	ve.lastSequence = atomic.LoadUint64(&vs.logSeqNum)
+	ve.NextFileNum = vs.nextFileNum
+	ve.LastSeqNum = atomic.LoadUint64(&vs.logSeqNum)
 	currentVersion := vs.currentVersion()
 	var newVersion *version
 
 	// Generate a new manifest if we don't currently have one, or the current one
 	// is too large.
-	var newManifestFileNumber uint64
+	var newManifestFileNum uint64
 	if vs.manifest == nil || vs.manifest.Size() >= vs.opts.MaxManifestFileSize {
-		newManifestFileNumber = vs.nextFileNum()
+		newManifestFileNum = vs.getNextFileNum()
 	}
 
 	var picker *compactionPicker
@@ -198,21 +218,21 @@ func (vs *versionSet) logAndApply(jobID int, ve *versionEdit, dir vfs.File) erro
 		defer vs.mu.Lock()
 
 		var bve bulkVersionEdit
-		bve.accumulate(ve)
+		bve.Accumulate(ve)
 
 		var err error
-		newVersion, err = bve.apply(vs.opts, currentVersion, vs.cmp)
+		newVersion, err = bve.Apply(vs.opts, currentVersion, vs.cmp)
 		if err != nil {
 			return err
 		}
 
-		if newManifestFileNumber != 0 {
-			if err := vs.createManifest(vs.dirname, newManifestFileNumber); err != nil {
+		if newManifestFileNum != 0 {
+			if err := vs.createManifest(vs.dirname, newManifestFileNum); err != nil {
 				if vs.opts.EventListener.ManifestCreated != nil {
 					vs.opts.EventListener.ManifestCreated(ManifestCreateInfo{
 						JobID:   jobID,
-						Path:    base.MakeFilename(vs.dirname, fileTypeManifest, newManifestFileNumber),
-						FileNum: newManifestFileNumber,
+						Path:    base.MakeFilename(vs.dirname, fileTypeManifest, newManifestFileNum),
+						FileNum: newManifestFileNum,
 						Err:     err,
 					})
 				}
@@ -229,7 +249,7 @@ func (vs *versionSet) logAndApply(jobID int, ve *versionEdit, dir vfs.File) erro
 		// fraught. Instead we rely on the standard recovery mechanism run when a
 		// database is open. In particular, that mechanism generates a new MANIFEST
 		// and ensures it is synced.
-		if err := ve.encode(w); err != nil {
+		if err := ve.Encode(w); err != nil {
 			vs.opts.Logger.Fatalf("MANIFEST write failed: %v", err)
 			return err
 		}
@@ -241,8 +261,8 @@ func (vs *versionSet) logAndApply(jobID int, ve *versionEdit, dir vfs.File) erro
 			vs.opts.Logger.Fatalf("MANIFEST sync failed: %v", err)
 			return err
 		}
-		if newManifestFileNumber != 0 {
-			if err := setCurrentFile(vs.dirname, vs.fs, newManifestFileNumber); err != nil {
+		if newManifestFileNum != 0 {
+			if err := setCurrentFile(vs.dirname, vs.fs, newManifestFileNum); err != nil {
 				vs.opts.Logger.Fatalf("MANIFEST set current failed: %v", err)
 				return err
 			}
@@ -253,8 +273,8 @@ func (vs *versionSet) logAndApply(jobID int, ve *versionEdit, dir vfs.File) erro
 			if vs.opts.EventListener.ManifestCreated != nil {
 				vs.opts.EventListener.ManifestCreated(ManifestCreateInfo{
 					JobID:   jobID,
-					Path:    base.MakeFilename(vs.dirname, fileTypeManifest, newManifestFileNumber),
-					FileNum: newManifestFileNumber,
+					Path:    base.MakeFilename(vs.dirname, fileTypeManifest, newManifestFileNum),
+					FileNum: newManifestFileNum,
 				})
 			}
 		}
@@ -269,29 +289,29 @@ func (vs *versionSet) logAndApply(jobID int, ve *versionEdit, dir vfs.File) erro
 
 	// Install the new version.
 	vs.append(newVersion)
-	if ve.logNumber != 0 {
-		vs.logNumber = ve.logNumber
+	if ve.LogNum != 0 {
+		vs.logNum = ve.LogNum
 	}
-	if ve.prevLogNumber != 0 {
-		vs.prevLogNumber = ve.prevLogNumber
+	if ve.PrevLogNum != 0 {
+		vs.prevLogNum = ve.PrevLogNum
 	}
-	if newManifestFileNumber != 0 {
-		if vs.manifestFileNumber != 0 {
-			vs.obsoleteManifests = append(vs.obsoleteManifests, vs.manifestFileNumber)
+	if newManifestFileNum != 0 {
+		if vs.manifestFileNum != 0 {
+			vs.obsoleteManifests = append(vs.obsoleteManifests, vs.manifestFileNum)
 		}
-		vs.manifestFileNumber = newManifestFileNumber
+		vs.manifestFileNum = newManifestFileNum
 	}
 	vs.picker = picker
 
-	if ve.metrics != nil {
-		for level, update := range ve.metrics {
+	if metrics != nil {
+		for level, update := range metrics {
 			vs.metrics.Levels[level].Add(update)
 		}
 	}
 	for i := range vs.metrics.Levels {
 		l := &vs.metrics.Levels[i]
-		l.NumFiles = int64(len(newVersion.files[i]))
-		l.Size = uint64(totalSize(newVersion.files[i]))
+		l.NumFiles = int64(len(newVersion.Files[i]))
+		l.Size = uint64(totalSize(newVersion.Files[i]))
 	}
 	return nil
 }
@@ -321,13 +341,13 @@ func (vs *versionSet) createManifest(dirname string, fileNum uint64) (err error)
 	manifest = record.NewWriter(manifestFile)
 
 	snapshot := versionEdit{
-		comparatorName: vs.cmpName,
+		ComparerName: vs.cmpName,
 	}
-	for level, fileMetadata := range vs.currentVersion().files {
+	for level, fileMetadata := range vs.currentVersion().Files {
 		for _, meta := range fileMetadata {
-			snapshot.newFiles = append(snapshot.newFiles, newFileEntry{
-				level: level,
-				meta:  meta,
+			snapshot.NewFiles = append(snapshot.NewFiles, newFileEntry{
+				Level: level,
+				Meta:  meta,
 			})
 		}
 	}
@@ -336,7 +356,7 @@ func (vs *versionSet) createManifest(dirname string, fileNum uint64) (err error)
 	if err1 != nil {
 		return err1
 	}
-	if err := snapshot.encode(w); err != nil {
+	if err := snapshot.Encode(w); err != nil {
 		return err
 	}
 
@@ -346,40 +366,44 @@ func (vs *versionSet) createManifest(dirname string, fileNum uint64) (err error)
 }
 
 func (vs *versionSet) markFileNumUsed(fileNum uint64) {
-	if vs.nextFileNumber <= fileNum {
-		vs.nextFileNumber = fileNum + 1
+	if vs.nextFileNum <= fileNum {
+		vs.nextFileNum = fileNum + 1
 	}
 }
 
-func (vs *versionSet) nextFileNum() uint64 {
-	x := vs.nextFileNumber
-	vs.nextFileNumber++
+func (vs *versionSet) getNextFileNum() uint64 {
+	x := vs.nextFileNum
+	vs.nextFileNum++
 	return x
 }
 
 func (vs *versionSet) append(v *version) {
-	if v.refs != 0 {
+	if v.Refs() != 0 {
 		panic("pebble: version should be unreferenced")
 	}
-	if !vs.versions.empty() {
-		vs.versions.back().unrefLocked()
+	if !vs.versions.Empty() {
+		vs.versions.Back().UnrefLocked()
 	}
-	v.vs = vs
-	v.ref()
-	vs.versions.pushBack(v)
+	v.Deleted = vs.obsoleteFn
+	v.Ref()
+	vs.versions.PushBack(v)
 }
 
 func (vs *versionSet) currentVersion() *version {
-	return vs.versions.back()
+	return vs.versions.Back()
 }
 
 func (vs *versionSet) addLiveFileNums(m map[uint64]struct{}) {
-	for v := vs.versions.root.next; v != &vs.versions.root; v = v.next {
-		for _, ff := range v.files {
+	current := vs.currentVersion()
+	for v := vs.versions.Front(); true; v = v.Next() {
+		for _, ff := range v.Files {
 			for _, f := range ff {
-				m[f.fileNum] = struct{}{}
+				m[f.FileNum] = struct{}{}
 			}
 		}
+		if v == current {
+			break
+		}
 	}
 }
 


Test patch:
diff --git a/compaction_picker_test.go b/compaction_picker_test.go
index 75c495c..33b1eaf 100644
--- a/compaction_picker_test.go
+++ b/compaction_picker_test.go
@@ -38,7 +38,7 @@ func loadVersion(d *datadriven.TestData) (*version, *Options, string) {
 			if err != nil {
 				return nil, nil, err.Error()
 			}
-			if vers.files[level] != nil {
+			if vers.Files[level] != nil {
 				return nil, nil, fmt.Sprintf("level %d already filled", level)
 			}
 			size, err := strconv.ParseUint(strings.TrimSpace(parts[1]), 10, 64)
@@ -47,13 +47,13 @@ func loadVersion(d *datadriven.TestData) (*version, *Options, string) {
 			}
 			if level == 0 {
 				for i := uint64(0); i < size; i++ {
-					vers.files[level] = append(vers.files[level], fileMetadata{
-						size: 1,
+					vers.Files[level] = append(vers.Files[level], fileMetadata{
+						Size: 1,
 					})
 				}
 			} else {
-				vers.files[level] = append(vers.files[level], fileMetadata{
-					size: size,
+				vers.Files[level] = append(vers.Files[level], fileMetadata{
+					Size: size,
 				})
 			}
 		}
diff --git a/compaction_test.go b/compaction_test.go
index 3416180..6b637fe 100644
--- a/compaction_test.go
+++ b/compaction_test.go
@@ -17,6 +17,7 @@ import (
 
 	"github.com/petermattis/pebble/internal/base"
 	"github.com/petermattis/pebble/internal/datadriven"
+	"github.com/petermattis/pebble/internal/manifest"
 	"github.com/petermattis/pebble/sstable"
 	"github.com/petermattis/pebble/vfs"
 )
@@ -25,7 +26,7 @@ func TestPickCompaction(t *testing.T) {
 	fileNums := func(f []fileMetadata) string {
 		ss := make([]string, 0, len(f))
 		for _, meta := range f {
-			ss = append(ss, strconv.Itoa(int(meta.fileNum)))
+			ss = append(ss, strconv.Itoa(int(meta.FileNum)))
 		}
 		sort.Strings(ss)
 		return strings.Join(ss, ",")
@@ -41,13 +42,13 @@ func TestPickCompaction(t *testing.T) {
 		{
 			desc: "no compaction",
 			version: version{
-				files: [numLevels][]fileMetadata{
+				Files: [numLevels][]fileMetadata{
 					0: []fileMetadata{
 						{
-							fileNum:  100,
-							size:     1,
-							smallest: base.ParseInternalKey("i.SET.101"),
-							largest:  base.ParseInternalKey("j.SET.102"),
+							FileNum:  100,
+							Size:     1,
+							Smallest: base.ParseInternalKey("i.SET.101"),
+							Largest:  base.ParseInternalKey("j.SET.102"),
 						},
 					},
 				},
@@ -58,13 +59,13 @@ func TestPickCompaction(t *testing.T) {
 		{
 			desc: "1 L0 file",
 			version: version{
-				files: [numLevels][]fileMetadata{
+				Files: [numLevels][]fileMetadata{
 					0: []fileMetadata{
 						{
-							fileNum:  100,
-							size:     1,
-							smallest: base.ParseInternalKey("i.SET.101"),
-							largest:  base.ParseInternalKey("j.SET.102"),
+							FileNum:  100,
+							Size:     1,
+							Smallest: base.ParseInternalKey("i.SET.101"),
+							Largest:  base.ParseInternalKey("j.SET.102"),
 						},
 					},
 				},
@@ -80,19 +81,19 @@ func TestPickCompaction(t *testing.T) {
 		{
 			desc: "2 L0 files (0 overlaps)",
 			version: version{
-				files: [numLevels][]fileMetadata{
+				Files: [numLevels][]fileMetadata{
 					0: []fileMetadata{
 						{
-							fileNum:  100,
-							size:     1,
-							smallest: base.ParseInternalKey("i.SET.101"),
-							largest:  base.ParseInternalKey("j.SET.102"),
+							FileNum:  100,
+							Size:     1,
+							Smallest: base.ParseInternalKey("i.SET.101"),
+							Largest:  base.ParseInternalKey("j.SET.102"),
 						},
 						{
-							fileNum:  110,
-							size:     1,
-							smallest: base.ParseInternalKey("k.SET.111"),
-							largest:  base.ParseInternalKey("l.SET.112"),
+							FileNum:  110,
+							Size:     1,
+							Smallest: base.ParseInternalKey("k.SET.111"),
+							Largest:  base.ParseInternalKey("l.SET.112"),
 						},
 					},
 				},
@@ -108,19 +109,19 @@ func TestPickCompaction(t *testing.T) {
 		{
 			desc: "2 L0 files, with ikey overlap",
 			version: version{
-				files: [numLevels][]fileMetadata{
+				Files: [numLevels][]fileMetadata{
 					0: []fileMetadata{
 						{
-							fileNum:  100,
-							size:     1,
-							smallest: base.ParseInternalKey("i.SET.101"),
-							largest:  base.ParseInternalKey("p.SET.102"),
+							FileNum:  100,
+							Size:     1,
+							Smallest: base.ParseInternalKey("i.SET.101"),
+							Largest:  base.ParseInternalKey("p.SET.102"),
 						},
 						{
-							fileNum:  110,
-							size:     1,
-							smallest: base.ParseInternalKey("j.SET.111"),
-							largest:  base.ParseInternalKey("q.SET.112"),
+							FileNum:  110,
+							Size:     1,
+							Smallest: base.ParseInternalKey("j.SET.111"),
+							Largest:  base.ParseInternalKey("q.SET.112"),
 						},
 					},
 				},
@@ -136,19 +137,19 @@ func TestPickCompaction(t *testing.T) {
 		{
 			desc: "2 L0 files, with ukey overlap",
 			version: version{
-				files: [numLevels][]fileMetadata{
+				Files: [numLevels][]fileMetadata{
 					0: []fileMetadata{
 						{
-							fileNum:  100,
-							size:     1,
-							smallest: base.ParseInternalKey("i.SET.101"),
-							largest:  base.ParseInternalKey("i.SET.102"),
+							FileNum:  100,
+							Size:     1,
+							Smallest: base.ParseInternalKey("i.SET.101"),
+							Largest:  base.ParseInternalKey("i.SET.102"),
 						},
 						{
-							fileNum:  110,
-							size:     1,
-							smallest: base.ParseInternalKey("i.SET.111"),
-							largest:  base.ParseInternalKey("i.SET.112"),
+							FileNum:  110,
+							Size:     1,
+							Smallest: base.ParseInternalKey("i.SET.111"),
+							Largest:  base.ParseInternalKey("i.SET.112"),
 						},
 					},
 				},
@@ -164,27 +165,27 @@ func TestPickCompaction(t *testing.T) {
 		{
 			desc: "1 L0 file, 2 L1 files (0 overlaps)",
 			version: version{
-				files: [numLevels][]fileMetadata{
+				Files: [numLevels][]fileMetadata{
 					0: []fileMetadata{
 						{
-							fileNum:  100,
-							size:     1,
-							smallest: base.ParseInternalKey("i.SET.101"),
-							largest:  base.ParseInternalKey("i.SET.102"),
+							FileNum:  100,
+							Size:     1,
+							Smallest: base.ParseInternalKey("i.SET.101"),
+							Largest:  base.ParseInternalKey("i.SET.102"),
 						},
 					},
 					1: []fileMetadata{
 						{
-							fileNum:  200,
-							size:     1,
-							smallest: base.ParseInternalKey("a.SET.201"),
-							largest:  base.ParseInternalKey("b.SET.202"),
+							FileNum:  200,
+							Size:     1,
+							Smallest: base.ParseInternalKey("a.SET.201"),
+							Largest:  base.ParseInternalKey("b.SET.202"),
 						},
 						{
-							fileNum:  210,
-							size:     1,
-							smallest: base.ParseInternalKey("y.SET.211"),
-							largest:  base.ParseInternalKey("z.SET.212"),
+							FileNum:  210,
+							Size:     1,
+							Smallest: base.ParseInternalKey("y.SET.211"),
+							Largest:  base.ParseInternalKey("z.SET.212"),
 						},
 					},
 				},
@@ -200,53 +201,53 @@ func TestPickCompaction(t *testing.T) {
 		{
 			desc: "1 L0 file, 2 L1 files (1 overlap), 4 L2 files (3 overlaps)",
 			version: version{
-				files: [numLevels][]fileMetadata{
+				Files: [numLevels][]fileMetadata{
 					0: []fileMetadata{
 						{
-							fileNum:  100,
-							size:     1,
-							smallest: base.ParseInternalKey("i.SET.101"),
-							largest:  base.ParseInternalKey("t.SET.102"),
+							FileNum:  100,
+							Size:     1,
+							Smallest: base.ParseInternalKey("i.SET.101"),
+							Largest:  base.ParseInternalKey("t.SET.102"),
 						},
 					},
 					1: []fileMetadata{
 						{
-							fileNum:  200,
-							size:     1,
-							smallest: base.ParseInternalKey("a.SET.201"),
-							largest:  base.ParseInternalKey("e.SET.202"),
+							FileNum:  200,
+							Size:     1,
+							Smallest: base.ParseInternalKey("a.SET.201"),
+							Largest:  base.ParseInternalKey("e.SET.202"),
 						},
 						{
-							fileNum:  210,
-							size:     1,
-							smallest: base.ParseInternalKey("f.SET.211"),
-							largest:  base.ParseInternalKey("j.SET.212"),
+							FileNum:  210,
+							Size:     1,
+							Smallest: base.ParseInternalKey("f.SET.211"),
+							Largest:  base.ParseInternalKey("j.SET.212"),
 						},
 					},
 					2: []fileMetadata{
 						{
-							fileNum:  300,
-							size:     1,
-							smallest: base.ParseInternalKey("a.SET.301"),
-							largest:  base.ParseInternalKey("b.SET.302"),
+							FileNum:  300,
+							Size:     1,
+							Smallest: base.ParseInternalKey("a.SET.301"),
+							Largest:  base.ParseInternalKey("b.SET.302"),
 						},
 						{
-							fileNum:  310,
-							size:     1,
-							smallest: base.ParseInternalKey("c.SET.311"),
-							largest:  base.ParseInternalKey("g.SET.312"),
+							FileNum:  310,
+							Size:     1,
+							Smallest: base.ParseInternalKey("c.SET.311"),
+							Largest:  base.ParseInternalKey("g.SET.312"),
 						},
 						{
-							fileNum:  320,
-							size:     1,
-							smallest: base.ParseInternalKey("h.SET.321"),
-							largest:  base.ParseInternalKey("m.SET.322"),
+							FileNum:  320,
+							Size:     1,
+							Smallest: base.ParseInternalKey("h.SET.321"),
+							Largest:  base.ParseInternalKey("m.SET.322"),
 						},
 						{
-							fileNum:  330,
-							size:     1,
-							smallest: base.ParseInternalKey("n.SET.331"),
-							largest:  base.ParseInternalKey("z.SET.332"),
+							FileNum:  330,
+							Size:     1,
+							Smallest: base.ParseInternalKey("n.SET.331"),
+							Largest:  base.ParseInternalKey("z.SET.332"),
 						},
 					},
 				},
@@ -262,45 +263,45 @@ func TestPickCompaction(t *testing.T) {
 		{
 			desc: "4 L1 files, 2 L2 files, can grow",
 			version: version{
-				files: [numLevels][]fileMetadata{
+				Files: [numLevels][]fileMetadata{
 					1: []fileMetadata{
 						{
-							fileNum:  200,
-							size:     1,
-							smallest: base.ParseInternalKey("i1.SET.201"),
-							largest:  base.ParseInternalKey("i2.SET.202"),
+							FileNum:  200,
+							Size:     1,
+							Smallest: base.ParseInternalKey("i1.SET.201"),
+							Largest:  base.ParseInternalKey("i2.SET.202"),
 						},
 						{
-							fileNum:  210,
-							size:     1,
-							smallest: base.ParseInternalKey("j1.SET.211"),
-							largest:  base.ParseInternalKey("j2.SET.212"),
+							FileNum:  210,
+							Size:     1,
+							Smallest: base.ParseInternalKey("j1.SET.211"),
+							Largest:  base.ParseInternalKey("j2.SET.212"),
 						},
 						{
-							fileNum:  220,
-							size:     1,
-							smallest: base.ParseInternalKey("k1.SET.221"),
-							largest:  base.ParseInternalKey("k2.SET.222"),
+							FileNum:  220,
+							Size:     1,
+							Smallest: base.ParseInternalKey("k1.SET.221"),
+							Largest:  base.ParseInternalKey("k2.SET.222"),
 						},
 						{
-							fileNum:  230,
-							size:     1,
-							smallest: base.ParseInternalKey("l1.SET.231"),
-							largest:  base.ParseInternalKey("l2.SET.232"),
+							FileNum:  230,
+							Size:     1,
+							Smallest: base.ParseInternalKey("l1.SET.231"),
+							Largest:  base.ParseInternalKey("l2.SET.232"),
 						},
 					},
 					2: []fileMetadata{
 						{
-							fileNum:  300,
-							size:     1,
-							smallest: base.ParseInternalKey("a0.SET.301"),
-							largest:  base.ParseInternalKey("l0.SET.302"),
+							FileNum:  300,
+							Size:     1,
+							Smallest: base.ParseInternalKey("a0.SET.301"),
+							Largest:  base.ParseInternalKey("l0.SET.302"),
 						},
 						{
-							fileNum:  310,
-							size:     1,
-							smallest: base.ParseInternalKey("l2.SET.311"),
-							largest:  base.ParseInternalKey("z2.SET.312"),
+							FileNum:  310,
+							Size:     1,
+							Smallest: base.ParseInternalKey("l2.SET.311"),
+							Largest:  base.ParseInternalKey("z2.SET.312"),
 						},
 					},
 				},
@@ -316,45 +317,45 @@ func TestPickCompaction(t *testing.T) {
 		{
 			desc: "4 L1 files, 2 L2 files, can't grow (range)",
 			version: version{
-				files: [numLevels][]fileMetadata{
+				Files: [numLevels][]fileMetadata{
 					1: []fileMetadata{
 						{
-							fileNum:  200,
-							size:     1,
-							smallest: base.ParseInternalKey("i1.SET.201"),
-							largest:  base.ParseInternalKey("i2.SET.202"),
+							FileNum:  200,
+							Size:     1,
+							Smallest: base.ParseInternalKey("i1.SET.201"),
+							Largest:  base.ParseInternalKey("i2.SET.202"),
 						},
 						{
-							fileNum:  210,
-							size:     1,
-							smallest: base.ParseInternalKey("j1.SET.211"),
-							largest:  base.ParseInternalKey("j2.SET.212"),
+							FileNum:  210,
+							Size:     1,
+							Smallest: base.ParseInternalKey("j1.SET.211"),
+							Largest:  base.ParseInternalKey("j2.SET.212"),
 						},
 						{
-							fileNum:  220,
-							size:     1,
-							smallest: base.ParseInternalKey("k1.SET.221"),
-							largest:  base.ParseInternalKey("k2.SET.222"),
+							FileNum:  220,
+							Size:     1,
+							Smallest: base.ParseInternalKey("k1.SET.221"),
+							Largest:  base.ParseInternalKey("k2.SET.222"),
 						},
 						{
-							fileNum:  230,
-							size:     1,
-							smallest: base.ParseInternalKey("l1.SET.231"),
-							largest:  base.ParseInternalKey("l2.SET.232"),
+							FileNum:  230,
+							Size:     1,
+							Smallest: base.ParseInternalKey("l1.SET.231"),
+							Largest:  base.ParseInternalKey("l2.SET.232"),
 						},
 					},
 					2: []fileMetadata{
 						{
-							fileNum:  300,
-							size:     1,
-							smallest: base.ParseInternalKey("a0.SET.301"),
-							largest:  base.ParseInternalKey("j0.SET.302"),
+							FileNum:  300,
+							Size:     1,
+							Smallest: base.ParseInternalKey("a0.SET.301"),
+							Largest:  base.ParseInternalKey("j0.SET.302"),
 						},
 						{
-							fileNum:  310,
-							size:     1,
-							smallest: base.ParseInternalKey("j2.SET.311"),
-							largest:  base.ParseInternalKey("z2.SET.312"),
+							FileNum:  310,
+							Size:     1,
+							Smallest: base.ParseInternalKey("j2.SET.311"),
+							Largest:  base.ParseInternalKey("z2.SET.312"),
 						},
 					},
 				},
@@ -370,45 +371,45 @@ func TestPickCompaction(t *testing.T) {
 		{
 			desc: "4 L1 files, 2 L2 files, can't grow (size)",
 			version: version{
-				files: [numLevels][]fileMetadata{
+				Files: [numLevels][]fileMetadata{
 					1: []fileMetadata{
 						{
-							fileNum:  200,
-							size:     expandedCompactionByteSizeLimit(opts, 1) - 1,
-							smallest: base.ParseInternalKey("i1.SET.201"),
-							largest:  base.ParseInternalKey("i2.SET.202"),
+							FileNum:  200,
+							Size:     expandedCompactionByteSizeLimit(opts, 1) - 1,
+							Smallest: base.ParseInternalKey("i1.SET.201"),
+							Largest:  base.ParseInternalKey("i2.SET.202"),
 						},
 						{
-							fileNum:  210,
-							size:     expandedCompactionByteSizeLimit(opts, 1) - 1,
-							smallest: base.ParseInternalKey("j1.SET.211"),
-							largest:  base.ParseInternalKey("j2.SET.212"),
+							FileNum:  210,
+							Size:     expandedCompactionByteSizeLimit(opts, 1) - 1,
+							Smallest: base.ParseInternalKey("j1.SET.211"),
+							Largest:  base.ParseInternalKey("j2.SET.212"),
 						},
 						{
-							fileNum:  220,
-							size:     expandedCompactionByteSizeLimit(opts, 1) - 1,
-							smallest: base.ParseInternalKey("k1.SET.221"),
-							largest:  base.ParseInternalKey("k2.SET.222"),
+							FileNum:  220,
+							Size:     expandedCompactionByteSizeLimit(opts, 1) - 1,
+							Smallest: base.ParseInternalKey("k1.SET.221"),
+							Largest:  base.ParseInternalKey("k2.SET.222"),
 						},
 						{
-							fileNum:  230,
-							size:     expandedCompactionByteSizeLimit(opts, 1) - 1,
-							smallest: base.ParseInternalKey("l1.SET.231"),
-							largest:  base.ParseInternalKey("l2.SET.232"),
+							FileNum:  230,
+							Size:     expandedCompactionByteSizeLimit(opts, 1) - 1,
+							Smallest: base.ParseInternalKey("l1.SET.231"),
+							Largest:  base.ParseInternalKey("l2.SET.232"),
 						},
 					},
 					2: []fileMetadata{
 						{
-							fileNum:  300,
-							size:     expandedCompactionByteSizeLimit(opts, 2) - 1,
-							smallest: base.ParseInternalKey("a0.SET.301"),
-							largest:  base.ParseInternalKey("l0.SET.302"),
+							FileNum:  300,
+							Size:     expandedCompactionByteSizeLimit(opts, 2) - 1,
+							Smallest: base.ParseInternalKey("a0.SET.301"),
+							Largest:  base.ParseInternalKey("l0.SET.302"),
 						},
 						{
-							fileNum:  310,
-							size:     expandedCompactionByteSizeLimit(opts, 2) - 1,
-							smallest: base.ParseInternalKey("l2.SET.311"),
-							largest:  base.ParseInternalKey("z2.SET.312"),
+							FileNum:  310,
+							Size:     expandedCompactionByteSizeLimit(opts, 2) - 1,
+							Smallest: base.ParseInternalKey("l2.SET.311"),
+							Largest:  base.ParseInternalKey("z2.SET.312"),
 						},
 					},
 				},
@@ -428,7 +429,7 @@ func TestPickCompaction(t *testing.T) {
 			cmp:     DefaultComparer.Compare,
 			cmpName: DefaultComparer.Name,
 		}
-		vs.versions.init()
+		vs.versions.Init(nil)
 		vs.append(&tc.version)
 		vs.picker = &tc.picker
 		vs.picker.vers = &tc.version
@@ -465,45 +466,45 @@ func TestElideTombstone(t *testing.T) {
 			desc:  "non-empty",
 			level: 1,
 			version: version{
-				files: [numLevels][]fileMetadata{
+				Files: [numLevels][]fileMetadata{
 					1: []fileMetadata{
 						{
-							smallest: base.ParseInternalKey("c.SET.801"),
-							largest:  base.ParseInternalKey("g.SET.800"),
+							Smallest: base.ParseInternalKey("c.SET.801"),
+							Largest:  base.ParseInternalKey("g.SET.800"),
 						},
 						{
-							smallest: base.ParseInternalKey("x.SET.701"),
-							largest:  base.ParseInternalKey("y.SET.700"),
+							Smallest: base.ParseInternalKey("x.SET.701"),
+							Largest:  base.ParseInternalKey("y.SET.700"),
 						},
 					},
 					2: []fileMetadata{
 						{
-							smallest: base.ParseInternalKey("d.SET.601"),
-							largest:  base.ParseInternalKey("h.SET.600"),
+							Smallest: base.ParseInternalKey("d.SET.601"),
+							Largest:  base.ParseInternalKey("h.SET.600"),
 						},
 						{
-							smallest: base.ParseInternalKey("r.SET.501"),
-							largest:  base.ParseInternalKey("t.SET.500"),
+							Smallest: base.ParseInternalKey("r.SET.501"),
+							Largest:  base.ParseInternalKey("t.SET.500"),
 						},
 					},
 					3: []fileMetadata{
 						{
-							smallest: base.ParseInternalKey("f.SET.401"),
-							largest:  base.ParseInternalKey("g.SET.400"),
+							Smallest: base.ParseInternalKey("f.SET.401"),
+							Largest:  base.ParseInternalKey("g.SET.400"),
 						},
 						{
-							smallest: base.ParseInternalKey("w.SET.301"),
-							largest:  base.ParseInternalKey("x.SET.300"),
+							Smallest: base.ParseInternalKey("w.SET.301"),
+							Largest:  base.ParseInternalKey("x.SET.300"),
 						},
 					},
 					4: []fileMetadata{
 						{
-							smallest: base.ParseInternalKey("f.SET.201"),
-							largest:  base.ParseInternalKey("m.SET.200"),
+							Smallest: base.ParseInternalKey("f.SET.201"),
+							Largest:  base.ParseInternalKey("m.SET.200"),
 						},
 						{
-							smallest: base.ParseInternalKey("t.SET.101"),
-							largest:  base.ParseInternalKey("t.SET.100"),
+							Smallest: base.ParseInternalKey("t.SET.101"),
+							Largest:  base.ParseInternalKey("t.SET.100"),
 						},
 					},
 				},
@@ -535,23 +536,23 @@ func TestElideTombstone(t *testing.T) {
 			desc:  "repeated ukey",
 			level: 1,
 			version: version{
-				files: [numLevels][]fileMetadata{
+				Files: [numLevels][]fileMetadata{
 					6: []fileMetadata{
 						{
-							smallest: base.ParseInternalKey("i.SET.401"),
-							largest:  base.ParseInternalKey("i.SET.400"),
+							Smallest: base.ParseInternalKey("i.SET.401"),
+							Largest:  base.ParseInternalKey("i.SET.400"),
 						},
 						{
-							smallest: base.ParseInternalKey("i.SET.301"),
-							largest:  base.ParseInternalKey("k.SET.300"),
+							Smallest: base.ParseInternalKey("i.SET.301"),
+							Largest:  base.ParseInternalKey("k.SET.300"),
 						},
 						{
-							smallest: base.ParseInternalKey("k.SET.201"),
-							largest:  base.ParseInternalKey("m.SET.200"),
+							Smallest: base.ParseInternalKey("k.SET.201"),
+							Largest:  base.ParseInternalKey("m.SET.200"),
 						},
 						{
-							smallest: base.ParseInternalKey("m.SET.101"),
-							largest:  base.ParseInternalKey("m.SET.100"),
+							Smallest: base.ParseInternalKey("m.SET.101"),
+							Largest:  base.ParseInternalKey("m.SET.100"),
 						},
 					},
 				},
@@ -620,14 +621,14 @@ func TestCompaction(t *testing.T) {
 		}
 		ss := []string(nil)
 		v := d.mu.versions.currentVersion()
-		for _, files := range v.files {
+		for _, files := range v.Files {
 			for _, meta := range files {
-				f, err := mem.Open(base.MakeFilename("", fileTypeTable, meta.fileNum))
+				f, err := mem.Open(base.MakeFilename("", fileTypeTable, meta.FileNum))
 				if err != nil {
 					return "", "", fmt.Errorf("Open: %v", err)
 				}
 				defer f.Close()
-				r, err := sstable.NewReader(f, 0, meta.fileNum, nil)
+				r, err := sstable.NewReader(f, 0, meta.FileNum, nil)
 				if err != nil {
 					return "", "", fmt.Errorf("NewReader: %v", err)
 				}
@@ -807,8 +808,8 @@ func TestCompactionShouldStopBefore(t *testing.T) {
 			t.Fatalf("malformed table spec: %s", s)
 		}
 		return fileMetadata{
-			smallest: InternalKey{UserKey: []byte(parts[0])},
-			largest:  InternalKey{UserKey: []byte(parts[1])},
+			Smallest: InternalKey{UserKey: []byte(parts[0])},
+			Largest:  InternalKey{UserKey: []byte(parts[1])},
 		}
 	}
 
@@ -828,13 +829,13 @@ func TestCompactionShouldStopBefore(t *testing.T) {
 
 					meta := parseMeta(parts[0])
 					var err error
-					meta.size, err = strconv.ParseUint(parts[1], 10, 64)
+					meta.Size, err = strconv.ParseUint(parts[1], 10, 64)
 					if err != nil {
 						return err.Error()
 					}
 					grandparents = append(grandparents, meta)
 				}
-				sort.Sort(bySmallest{grandparents, cmp})
+				manifest.SortBySmallest(grandparents, cmp)
 				return ""
 
 			case "compact":
@@ -912,8 +913,8 @@ func TestCompactionExpandInputs(t *testing.T) {
 			t.Fatalf("malformed table spec: %s", s)
 		}
 		return fileMetadata{
-			smallest: base.ParseInternalKey(parts[0]),
-			largest:  base.ParseInternalKey(parts[1]),
+			Smallest: base.ParseInternalKey(parts[0]),
+			Largest:  base.ParseInternalKey(parts[1]),
 		}
 	}
 
@@ -927,10 +928,10 @@ func TestCompactionExpandInputs(t *testing.T) {
 				}
 				for _, data := range strings.Split(d.Input, "\n") {
 					meta := parseMeta(data)
-					meta.fileNum = uint64(len(files))
+					meta.FileNum = uint64(len(files))
 					files = append(files, meta)
 				}
-				sort.Sort(bySmallest{files, cmp})
+				manifest.SortBySmallest(files, cmp)
 				return ""
 
 			case "expand-inputs":
@@ -939,7 +940,7 @@ func TestCompactionExpandInputs(t *testing.T) {
 					version:    &version{},
 					startLevel: 1,
 				}
-				c.version.files[c.startLevel] = files
+				c.version.Files[c.startLevel] = files
 				if len(d.CmdArgs) != 1 {
 					return fmt.Sprintf("%s expects 1 argument", d.Cmd)
 				}
@@ -953,7 +954,7 @@ func TestCompactionExpandInputs(t *testing.T) {
 				var buf bytes.Buffer
 				for i := range inputs {
 					f := &inputs[i]
-					fmt.Fprintf(&buf, "%d: %s-%s\n", f.fileNum, f.smallest, f.largest)
+					fmt.Fprintf(&buf, "%d: %s-%s\n", f.FileNum, f.Smallest, f.Largest)
 				}
 				return buf.String()
 
@@ -973,8 +974,8 @@ func TestCompactionAtomicUnitBounds(t *testing.T) {
 			t.Fatalf("malformed table spec: %s", s)
 		}
 		return fileMetadata{
-			smallest: base.ParseInternalKey(parts[0]),
-			largest:  base.ParseInternalKey(parts[1]),
+			Smallest: base.ParseInternalKey(parts[0]),
+			Largest:  base.ParseInternalKey(parts[1]),
 		}
 	}
 
@@ -988,10 +989,10 @@ func TestCompactionAtomicUnitBounds(t *testing.T) {
 				}
 				for _, data := range strings.Split(d.Input, "\n") {
 					meta := parseMeta(data)
-					meta.fileNum = uint64(len(files))
+					meta.FileNum = uint64(len(files))
 					files = append(files, meta)
 				}
-				sort.Sort(bySmallest{files, cmp})
+				manifest.SortBySmallest(files, cmp)
 				return ""
 
 			case "atomic-unit-bounds":
@@ -1030,8 +1031,8 @@ func TestCompactionAllowZeroSeqNum(t *testing.T) {
 			t.Fatalf("malformed table spec: %s: %s", s, err)
 		}
 		meta = fileMetadata{
-			smallest: InternalKey{UserKey: []byte(match[2])},
-			largest:  InternalKey{UserKey: []byte(match[3])},
+			Smallest: InternalKey{UserKey: []byte(match[2])},
+			Largest:  InternalKey{UserKey: []byte(match[3])},
 		}
 		return level, meta
 	}
diff --git a/data_test.go b/data_test.go
index 62356fe..f15b61b 100644
--- a/data_test.go
+++ b/data_test.go
@@ -308,10 +308,10 @@ func runDBDefineCmd(td *datadriven.TestData, opts *Options) (*DB, error) {
 		if err != nil {
 			return nil
 		}
-		for _, f := range newVE.newFiles {
-			ve.newFiles = append(ve.newFiles, newFileEntry{
-				level: level,
-				meta:  f.meta,
+		for _, f := range newVE.NewFiles {
+			ve.NewFiles = append(ve.NewFiles, newFileEntry{
+				Level: level,
+				Meta:  f.Meta,
 			})
 		}
 		level = -1
@@ -361,16 +361,16 @@ func runDBDefineCmd(td *datadriven.TestData, opts *Options) (*DB, error) {
 		return nil, err
 	}
 
-	if len(ve.newFiles) > 0 {
+	if len(ve.NewFiles) > 0 {
 		jobID := d.mu.nextJobID
 		d.mu.nextJobID++
-		if err := d.mu.versions.logAndApply(jobID, ve, d.dataDir); err != nil {
+		if err := d.mu.versions.logAndApply(jobID, ve, nil, d.dataDir); err != nil {
 			return nil, err
 		}
 		d.updateReadStateLocked()
-		for i := range ve.newFiles {
-			meta := &ve.newFiles[i].meta
-			delete(d.mu.compact.pendingOutputs, meta.fileNum)
+		for i := range ve.NewFiles {
+			meta := &ve.NewFiles[i].Meta
+			delete(d.mu.compact.pendingOutputs, meta.FileNum)
 		}
 	}
 
diff --git a/db_test.go b/db_test.go
index 6bb6fb7..52b0ae9 100644
--- a/db_test.go
+++ b/db_test.go
@@ -629,7 +629,7 @@ func TestRollManifest(t *testing.T) {
 	manifestFileNumber := func() uint64 {
 		d.mu.Lock()
 		defer d.mu.Unlock()
-		return d.mu.versions.manifestFileNumber
+		return d.mu.versions.manifestFileNum
 	}
 
 	current := func() string {
diff --git a/get_iter_test.go b/get_iter_test.go
index cd4ac56..1d0227f 100644
--- a/get_iter_test.go
+++ b/get_iter_test.go
@@ -471,7 +471,7 @@ func TestGetIter(t *testing.T) {
 		newIter := func(
 			meta *fileMetadata, _ *IterOptions, _ *uint64,
 		) (internalIterator, internalIterator, error) {
-			d, ok := m[meta.fileNum]
+			d, ok := m[meta.FileNum]
 			if !ok {
 				return nil, nil, errors.New("no such file")
 			}
@@ -485,7 +485,7 @@ func TestGetIter(t *testing.T) {
 			m[tt.fileNum] = d
 
 			meta := fileMetadata{
-				fileNum: tt.fileNum,
+				FileNum: tt.fileNum,
 			}
 			for i, datum := range tt.data {
 				s := strings.Split(datum, " ")
@@ -496,30 +496,30 @@ func TestGetIter(t *testing.T) {
 				}
 
 				if i == 0 {
-					meta.smallest = ikey
-					meta.smallestSeqNum = ikey.SeqNum()
-					meta.largest = ikey
-					meta.largestSeqNum = ikey.SeqNum()
+					meta.Smallest = ikey
+					meta.SmallestSeqNum = ikey.SeqNum()
+					meta.Largest = ikey
+					meta.LargestSeqNum = ikey.SeqNum()
 				} else {
-					if base.InternalCompare(cmp, ikey, meta.smallest) < 0 {
-						meta.smallest = ikey
+					if base.InternalCompare(cmp, ikey, meta.Smallest) < 0 {
+						meta.Smallest = ikey
 					}
-					if base.InternalCompare(cmp, ikey, meta.largest) > 0 {
-						meta.largest = ikey
+					if base.InternalCompare(cmp, ikey, meta.Largest) > 0 {
+						meta.Largest = ikey
 					}
-					if meta.smallestSeqNum > ikey.SeqNum() {
-						meta.smallestSeqNum = ikey.SeqNum()
+					if meta.SmallestSeqNum > ikey.SeqNum() {
+						meta.SmallestSeqNum = ikey.SeqNum()
 					}
-					if meta.largestSeqNum < ikey.SeqNum() {
-						meta.largestSeqNum = ikey.SeqNum()
+					if meta.LargestSeqNum < ikey.SeqNum() {
+						meta.LargestSeqNum = ikey.SeqNum()
 					}
 				}
 			}
 
-			v.files[tt.level] = append(v.files[tt.level], meta)
+			v.Files[tt.level] = append(v.Files[tt.level], meta)
 		}
 
-		err := v.checkOrdering(cmp)
+		err := v.CheckOrdering(cmp)
 		if tc.badOrdering && err == nil {
 			t.Errorf("desc=%q: want bad ordering, got nil error", desc)
 			continue
@@ -539,7 +539,7 @@ func TestGetIter(t *testing.T) {
 			get.equal = equal
 			get.newIters = newIter
 			get.key = ikey.UserKey
-			get.l0 = v.files[0]
+			get.l0 = v.Files[0]
 			get.version = v
 			get.snapshot = ikey.SeqNum() + 1
 
diff --git a/ingest_test.go b/ingest_test.go
index f937070..b9554a0 100644
--- a/ingest_test.go
+++ b/ingest_test.go
@@ -17,6 +17,7 @@ import (
 	"github.com/kr/pretty"
 	"github.com/petermattis/pebble/internal/base"
 	"github.com/petermattis/pebble/internal/datadriven"
+	"github.com/petermattis/pebble/internal/manifest"
 	"github.com/petermattis/pebble/sstable"
 	"github.com/petermattis/pebble/vfs"
 	"golang.org/x/exp/rand"
@@ -56,7 +57,7 @@ func TestIngestLoad(t *testing.T) {
 			}
 			var buf bytes.Buffer
 			for _, m := range meta {
-				fmt.Fprintf(&buf, "%d: %s-%s\n", m.fileNum, m.smallest, m.largest)
+				fmt.Fprintf(&buf, "%d: %s-%s\n", m.FileNum, m.Smallest, m.Largest)
 			}
 			return buf.String()
 
@@ -86,7 +87,7 @@ func TestIngestLoadRand(t *testing.T) {
 		paths[i] = fmt.Sprint(i)
 		pending[i] = uint64(rng.Int63())
 		expected[i] = &fileMetadata{
-			fileNum: pending[i],
+			FileNum: pending[i],
 		}
 
 		func() {
@@ -107,8 +108,8 @@ func TestIngestLoadRand(t *testing.T) {
 				return base.InternalCompare(cmp, keys[i], keys[j]) < 0
 			})
 
-			expected[i].smallest = keys[0]
-			expected[i].largest = keys[len(keys)-1]
+			expected[i].Smallest = keys[0]
+			expected[i].Largest = keys[len(keys)-1]
 
 			w := sstable.NewWriter(f, nil, LevelOptions{})
 			for i := range keys {
@@ -122,7 +123,7 @@ func TestIngestLoadRand(t *testing.T) {
 			if err != nil {
 				t.Fatal(err)
 			}
-			expected[i].size = meta.Size
+			expected[i].Size = meta.Size
 		}()
 	}
 
@@ -197,8 +198,8 @@ func TestIngestSortAndVerify(t *testing.T) {
 						return fmt.Sprintf("range %v-%v is not valid", smallest, largest)
 					}
 					meta = append(meta, &fileMetadata{
-						smallest: smallest,
-						largest:  largest,
+						Smallest: smallest,
+						Largest:  largest,
 					})
 				}
 				err := ingestSortAndVerify(cmp, meta)
@@ -206,7 +207,7 @@ func TestIngestSortAndVerify(t *testing.T) {
 					return fmt.Sprintf("%v\n", err)
 				}
 				for i := range meta {
-					fmt.Fprintf(&buf, "%v-%v\n", meta[i].smallest, meta[i].largest)
+					fmt.Fprintf(&buf, "%v-%v\n", meta[i].Smallest, meta[i].Largest)
 				}
 				return buf.String()
 
@@ -238,7 +239,7 @@ func TestIngestLink(t *testing.T) {
 			for j := range paths {
 				paths[j] = fmt.Sprintf("external%d", j)
 				meta[j] = &fileMetadata{}
-				meta[j].fileNum = uint64(j)
+				meta[j].FileNum = uint64(j)
 				f, err := mem.Create(paths[j])
 				if err != nil {
 					t.Fatal(err)
@@ -333,8 +334,8 @@ func TestIngestMemtableOverlaps(t *testing.T) {
 					parts[0], parts[1] = parts[1], parts[0]
 				}
 				return &fileMetadata{
-					smallest: InternalKey{UserKey: []byte(parts[0])},
-					largest:  InternalKey{UserKey: []byte(parts[1])},
+					Smallest: InternalKey{UserKey: []byte(parts[0])},
+					Largest:  InternalKey{UserKey: []byte(parts[1])},
 				}
 			}
 
@@ -395,8 +396,8 @@ func TestIngestTargetLevel(t *testing.T) {
 			t.Fatalf("malformed table spec: %s", s)
 		}
 		return fileMetadata{
-			smallest: InternalKey{UserKey: []byte(parts[0])},
-			largest:  InternalKey{UserKey: []byte(parts[1])},
+			Smallest: InternalKey{UserKey: []byte(parts[0])},
+			Largest:  InternalKey{UserKey: []byte(parts[1])},
 		}
 	}
 
@@ -416,17 +417,17 @@ func TestIngestTargetLevel(t *testing.T) {
 				if err != nil {
 					return err.Error()
 				}
-				if vers.files[level] != nil {
+				if vers.Files[level] != nil {
 					return fmt.Sprintf("level %d already filled", level)
 				}
 				for _, table := range strings.Fields(parts[1]) {
-					vers.files[level] = append(vers.files[level], parseMeta(table))
+					vers.Files[level] = append(vers.Files[level], parseMeta(table))
 				}
 
 				if level == 0 {
-					sort.Sort(bySeqNum(vers.files[level]))
+					manifest.SortBySeqNum(vers.Files[level])
 				} else {
-					sort.Sort(bySmallest{vers.files[level], cmp})
+					manifest.SortBySmallest(vers.Files[level], cmp)
 				}
 			}
 			return ""
diff --git a/version_edit_test.go b/internal/manifest/version_edit_test.go
similarity index 64%
rename from version_edit_test.go
rename to internal/manifest/version_edit_test.go
index da163d6..6d1ea9b 100644
--- a/version_edit_test.go
+++ b/internal/manifest/version_edit_test.go
@@ -2,7 +2,7 @@
 // of this source code is governed by a BSD-style license that can be found in
 // the LICENSE file.
 
-package pebble
+package manifest
 
 import (
 	"bytes"
@@ -19,13 +19,13 @@ import (
 	"github.com/petermattis/pebble/internal/record"
 )
 
-func checkRoundTrip(e0 versionEdit) error {
-	var e1 versionEdit
+func checkRoundTrip(e0 VersionEdit) error {
+	var e1 VersionEdit
 	buf := new(bytes.Buffer)
-	if err := e0.encode(buf); err != nil {
+	if err := e0.Encode(buf); err != nil {
 		return fmt.Errorf("encode: %v", err)
 	}
-	if err := e1.decode(buf); err != nil {
+	if err := e1.Decode(buf); err != nil {
 		return fmt.Errorf("decode: %v", err)
 	}
 	if !reflect.DeepEqual(e1, e0) {
@@ -35,46 +35,46 @@ func checkRoundTrip(e0 versionEdit) error {
 }
 
 func TestVersionEditRoundTrip(t *testing.T) {
-	testCases := []versionEdit{
+	testCases := []VersionEdit{
 		// An empty version edit.
 		{},
 		// A complete version edit.
 		{
-			comparatorName: "11",
-			logNumber:      22,
-			prevLogNumber:  33,
-			nextFileNumber: 44,
-			lastSequence:   55,
-			deletedFiles: map[deletedFileEntry]bool{
-				deletedFileEntry{
-					level:   3,
-					fileNum: 703,
+			ComparerName: "11",
+			LogNum:       22,
+			PrevLogNum:   33,
+			NextFileNum:  44,
+			LastSeqNum:   55,
+			DeletedFiles: map[DeletedFileEntry]bool{
+				DeletedFileEntry{
+					Level:   3,
+					FileNum: 703,
 				}: true,
-				deletedFileEntry{
-					level:   4,
-					fileNum: 704,
+				DeletedFileEntry{
+					Level:   4,
+					FileNum: 704,
 				}: true,
 			},
-			newFiles: []newFileEntry{
+			NewFiles: []NewFileEntry{
 				{
-					level: 5,
-					meta: fileMetadata{
-						fileNum:  805,
-						size:     8050,
-						smallest: base.DecodeInternalKey([]byte("abc\x00\x01\x02\x03\x04\x05\x06\x07")),
-						largest:  base.DecodeInternalKey([]byte("xyz\x01\xff\xfe\xfd\xfc\xfb\xfa\xf9")),
+					Level: 5,
+					Meta: FileMetadata{
+						FileNum:  805,
+						Size:     8050,
+						Smallest: base.DecodeInternalKey([]byte("abc\x00\x01\x02\x03\x04\x05\x06\x07")),
+						Largest:  base.DecodeInternalKey([]byte("xyz\x01\xff\xfe\xfd\xfc\xfb\xfa\xf9")),
 					},
 				},
 				{
-					level: 6,
-					meta: fileMetadata{
-						fileNum:             806,
-						size:                8060,
-						smallest:            base.DecodeInternalKey([]byte("A\x00\x01\x02\x03\x04\x05\x06\x07")),
-						largest:             base.DecodeInternalKey([]byte("Z\x01\xff\xfe\xfd\xfc\xfb\xfa\xf9")),
-						smallestSeqNum:      3,
-						largestSeqNum:       5,
-						markedForCompaction: true,
+					Level: 6,
+					Meta: FileMetadata{
+						FileNum:             806,
+						Size:                8060,
+						Smallest:            base.DecodeInternalKey([]byte("A\x00\x01\x02\x03\x04\x05\x06\x07")),
+						Largest:             base.DecodeInternalKey([]byte("Z\x01\xff\xfe\xfd\xfc\xfb\xfa\xf9")),
+						SmallestSeqNum:      3,
+						LargestSeqNum:       5,
+						MarkedForCompaction: true,
 					},
 				},
 			},
@@ -91,7 +91,7 @@ func TestVersionEditDecode(t *testing.T) {
 	testCases := []struct {
 		filename     string
 		encodedEdits []string
-		edits        []versionEdit
+		edits        []VersionEdit
 	}{
 		// db-stage-1 and db-stage-2 have the same manifest.
 		{
@@ -99,9 +99,9 @@ func TestVersionEditDecode(t *testing.T) {
 			encodedEdits: []string{
 				"\x02\x00\x03\x02\x04\x00",
 			},
-			edits: []versionEdit{
+			edits: []VersionEdit{
 				{
-					nextFileNumber: 2,
+					NextFileNum: 2,
 				},
 			},
 		},
@@ -115,26 +115,26 @@ func TestVersionEditDecode(t *testing.T) {
 					"\x00\x05\x00\x00\x00\x00\x00\x00\vfoo\x01\x04\x00" +
 					"\x00\x00\x00\x00\x00\x03\x05",
 			},
-			edits: []versionEdit{
+			edits: []VersionEdit{
 				{
-					comparatorName: "leveldb.BytewiseComparator",
+					ComparerName: "leveldb.BytewiseComparator",
 				},
 				{},
 				{
-					logNumber:      4,
-					prevLogNumber:  0,
-					nextFileNumber: 6,
-					lastSequence:   5,
-					newFiles: []newFileEntry{
+					LogNum:      4,
+					PrevLogNum:  0,
+					NextFileNum: 6,
+					LastSeqNum:  5,
+					NewFiles: []NewFileEntry{
 						{
-							level: 0,
-							meta: fileMetadata{
-								fileNum:        4,
-								size:           986,
-								smallest:       base.MakeInternalKey([]byte("bar"), 5, InternalKeyKindDelete),
-								largest:        base.MakeInternalKey([]byte("foo"), 4, InternalKeyKindSet),
-								smallestSeqNum: 3,
-								largestSeqNum:  5,
+							Level: 0,
+							Meta: FileMetadata{
+								FileNum:        4,
+								Size:           986,
+								Smallest:       base.MakeInternalKey([]byte("bar"), 5, base.InternalKeyKindDelete),
+								Largest:        base.MakeInternalKey([]byte("foo"), 4, base.InternalKeyKindSet),
+								SmallestSeqNum: 3,
+								LargestSeqNum:  5,
 							},
 						},
 					},
@@ -145,7 +145,7 @@ func TestVersionEditDecode(t *testing.T) {
 
 	for _, tc := range testCases {
 		t.Run("", func(t *testing.T) {
-			f, err := os.Open("testdata/" + tc.filename)
+			f, err := os.Open("../../testdata/" + tc.filename)
 			if err != nil {
 				t.Fatalf("filename=%q: open error: %v", tc.filename, err)
 			}
@@ -171,8 +171,8 @@ func TestVersionEditDecode(t *testing.T) {
 					t.Fatalf("filename=%q i=%d: got encoded %q, want %q", tc.filename, i, s, tc.encodedEdits[i])
 				}
 
-				var edit versionEdit
-				err = edit.decode(bytes.NewReader(encodedEdit))
+				var edit VersionEdit
+				err = edit.Decode(bytes.NewReader(encodedEdit))
 				if err != nil {
 					t.Fatalf("filename=%q i=%d: decode error: %v", tc.filename, i, err)
 				}
diff --git a/version_test.go b/internal/manifest/version_test.go
similarity index 58%
rename from version_test.go
rename to internal/manifest/version_test.go
index ea7a8c1..71ece03 100644
--- a/version_test.go
+++ b/internal/manifest/version_test.go
@@ -2,7 +2,7 @@
 // of this source code is governed by a BSD-style license that can be found in
 // the LICENSE file.
 
-package pebble
+package manifest
 
 import (
 	"fmt"
@@ -13,6 +13,10 @@ import (
 	"github.com/petermattis/pebble/internal/base"
 )
 
+func ikey(s string) InternalKey {
+	return base.MakeInternalKey([]byte(s), 0, base.InternalKeyKindSet)
+}
+
 func TestIkeyRange(t *testing.T) {
 	testCases := []struct {
 		input, want string
@@ -55,23 +59,23 @@ func TestIkeyRange(t *testing.T) {
 		},
 	}
 	for _, tc := range testCases {
-		var f []fileMetadata
+		var f []FileMetadata
 		if tc.input != "" {
 			for _, s := range strings.Split(tc.input, " ") {
-				f = append(f, fileMetadata{
-					smallest: ikey(s[0:1]),
-					largest:  ikey(s[2:3]),
+				f = append(f, FileMetadata{
+					Smallest: ikey(s[0:1]),
+					Largest:  ikey(s[2:3]),
 				})
 			}
 		}
 
-		smallest0, largest0 := ikeyRange(DefaultComparer.Compare, f, nil)
+		smallest0, largest0 := KeyRange(base.DefaultComparer.Compare, f, nil)
 		got0 := string(smallest0.UserKey) + "-" + string(largest0.UserKey)
 		if got0 != tc.want {
 			t.Errorf("first []fileMetadata is %v\ngot  %s\nwant %s", tc.input, got0, tc.want)
 		}
 
-		smallest1, largest1 := ikeyRange(DefaultComparer.Compare, nil, f)
+		smallest1, largest1 := KeyRange(base.DefaultComparer.Compare, nil, f)
 		got1 := string(smallest1.UserKey) + "-" + string(largest1.UserKey)
 		if got1 != tc.want {
 			t.Errorf("second []fileMetadata is %v\ngot  %s\nwant %s", tc.input, got1, tc.want)
@@ -80,88 +84,88 @@ func TestIkeyRange(t *testing.T) {
 }
 
 func TestOverlaps(t *testing.T) {
-	m00 := fileMetadata{
-		fileNum:  700,
-		size:     1,
-		smallest: base.ParseInternalKey("b.SET.7008"),
-		largest:  base.ParseInternalKey("e.SET.7009"),
+	m00 := FileMetadata{
+		FileNum:  700,
+		Size:     1,
+		Smallest: base.ParseInternalKey("b.SET.7008"),
+		Largest:  base.ParseInternalKey("e.SET.7009"),
 	}
-	m01 := fileMetadata{
-		fileNum:  701,
-		size:     1,
-		smallest: base.ParseInternalKey("c.SET.7018"),
-		largest:  base.ParseInternalKey("f.SET.7019"),
+	m01 := FileMetadata{
+		FileNum:  701,
+		Size:     1,
+		Smallest: base.ParseInternalKey("c.SET.7018"),
+		Largest:  base.ParseInternalKey("f.SET.7019"),
 	}
-	m02 := fileMetadata{
-		fileNum:  702,
-		size:     1,
-		smallest: base.ParseInternalKey("f.SET.7028"),
-		largest:  base.ParseInternalKey("g.SET.7029"),
+	m02 := FileMetadata{
+		FileNum:  702,
+		Size:     1,
+		Smallest: base.ParseInternalKey("f.SET.7028"),
+		Largest:  base.ParseInternalKey("g.SET.7029"),
 	}
-	m03 := fileMetadata{
-		fileNum:  703,
-		size:     1,
-		smallest: base.ParseInternalKey("x.SET.7038"),
-		largest:  base.ParseInternalKey("y.SET.7039"),
+	m03 := FileMetadata{
+		FileNum:  703,
+		Size:     1,
+		Smallest: base.ParseInternalKey("x.SET.7038"),
+		Largest:  base.ParseInternalKey("y.SET.7039"),
 	}
-	m04 := fileMetadata{
-		fileNum:  704,
-		size:     1,
-		smallest: base.ParseInternalKey("n.SET.7048"),
-		largest:  base.ParseInternalKey("p.SET.7049"),
+	m04 := FileMetadata{
+		FileNum:  704,
+		Size:     1,
+		Smallest: base.ParseInternalKey("n.SET.7048"),
+		Largest:  base.ParseInternalKey("p.SET.7049"),
 	}
-	m05 := fileMetadata{
-		fileNum:  705,
-		size:     1,
-		smallest: base.ParseInternalKey("p.SET.7058"),
-		largest:  base.ParseInternalKey("p.SET.7059"),
+	m05 := FileMetadata{
+		FileNum:  705,
+		Size:     1,
+		Smallest: base.ParseInternalKey("p.SET.7058"),
+		Largest:  base.ParseInternalKey("p.SET.7059"),
 	}
-	m06 := fileMetadata{
-		fileNum:  706,
-		size:     1,
-		smallest: base.ParseInternalKey("p.SET.7068"),
-		largest:  base.ParseInternalKey("u.SET.7069"),
+	m06 := FileMetadata{
+		FileNum:  706,
+		Size:     1,
+		Smallest: base.ParseInternalKey("p.SET.7068"),
+		Largest:  base.ParseInternalKey("u.SET.7069"),
 	}
-	m07 := fileMetadata{
-		fileNum:  707,
-		size:     1,
-		smallest: base.ParseInternalKey("r.SET.7078"),
-		largest:  base.ParseInternalKey("s.SET.7079"),
+	m07 := FileMetadata{
+		FileNum:  707,
+		Size:     1,
+		Smallest: base.ParseInternalKey("r.SET.7078"),
+		Largest:  base.ParseInternalKey("s.SET.7079"),
 	}
 
-	m10 := fileMetadata{
-		fileNum:  710,
-		size:     1,
-		smallest: base.ParseInternalKey("d.SET.7108"),
-		largest:  base.ParseInternalKey("g.SET.7109"),
+	m10 := FileMetadata{
+		FileNum:  710,
+		Size:     1,
+		Smallest: base.ParseInternalKey("d.SET.7108"),
+		Largest:  base.ParseInternalKey("g.SET.7109"),
 	}
-	m11 := fileMetadata{
-		fileNum:  711,
-		size:     1,
-		smallest: base.ParseInternalKey("g.SET.7118"),
-		largest:  base.ParseInternalKey("j.SET.7119"),
+	m11 := FileMetadata{
+		FileNum:  711,
+		Size:     1,
+		Smallest: base.ParseInternalKey("g.SET.7118"),
+		Largest:  base.ParseInternalKey("j.SET.7119"),
 	}
-	m12 := fileMetadata{
-		fileNum:  712,
-		size:     1,
-		smallest: base.ParseInternalKey("n.SET.7128"),
-		largest:  base.ParseInternalKey("p.SET.7129"),
+	m12 := FileMetadata{
+		FileNum:  712,
+		Size:     1,
+		Smallest: base.ParseInternalKey("n.SET.7128"),
+		Largest:  base.ParseInternalKey("p.SET.7129"),
 	}
-	m13 := fileMetadata{
-		fileNum:  713,
-		size:     1,
-		smallest: base.ParseInternalKey("p.SET.7138"),
-		largest:  base.ParseInternalKey("p.SET.7139"),
+	m13 := FileMetadata{
+		FileNum:  713,
+		Size:     1,
+		Smallest: base.ParseInternalKey("p.SET.7138"),
+		Largest:  base.ParseInternalKey("p.SET.7139"),
 	}
-	m14 := fileMetadata{
-		fileNum:  714,
-		size:     1,
-		smallest: base.ParseInternalKey("p.SET.7148"),
-		largest:  base.ParseInternalKey("u.SET.7149"),
+	m14 := FileMetadata{
+		FileNum:  714,
+		Size:     1,
+		Smallest: base.ParseInternalKey("p.SET.7148"),
+		Largest:  base.ParseInternalKey("u.SET.7149"),
 	}
 
-	v := version{
-		files: [numLevels][]fileMetadata{
+	v := Version{
+		Files: [NumLevels][]FileMetadata{
 			0: {m00, m01, m02, m03, m04, m05, m06, m07},
 			1: {m10, m11, m12, m13, m14},
 		},
@@ -238,12 +242,12 @@ func TestOverlaps(t *testing.T) {
 		{2, "a", "z", ""},
 	}
 
-	cmp := DefaultComparer.Compare
+	cmp := base.DefaultComparer.Compare
 	for _, tc := range testCases {
-		o := v.overlaps(tc.level, cmp, []byte(tc.ukey0), []byte(tc.ukey1))
+		o := v.Overlaps(tc.level, cmp, []byte(tc.ukey0), []byte(tc.ukey1))
 		s := make([]string, len(o))
 		for i, meta := range o {
-			s[i] = fmt.Sprintf("m%02d", meta.fileNum%100)
+			s[i] = fmt.Sprintf("m%02d", meta.FileNum%100)
 		}
 		got := strings.Join(s, " ")
 		if got != tc.want {
@@ -253,15 +257,13 @@ func TestOverlaps(t *testing.T) {
 }
 
 func TestVersionUnref(t *testing.T) {
-	list := &versionList{
-		mu: &sync.Mutex{},
-	}
-	list.init()
-	v := &version{vs: &versionSet{}}
-	v.ref()
-	list.pushBack(v)
-	v.unref()
-	if !list.empty() {
+	list := &VersionList{}
+	list.Init(&sync.Mutex{})
+	v := &Version{Deleted: func([]uint64) {}}
+	v.Ref()
+	list.PushBack(v)
+	v.Unref()
+	if !list.Empty() {
 		t.Fatalf("expected version list to be empty")
 	}
 }
diff --git a/level_iter_test.go b/level_iter_test.go
index cbadb73..4125488 100644
--- a/level_iter_test.go
+++ b/level_iter_test.go
@@ -27,7 +27,7 @@ func TestLevelIter(t *testing.T) {
 	newIters := func(
 		meta *fileMetadata, opts *IterOptions, bytesIterated *uint64,
 	) (internalIterator, internalIterator, error) {
-		f := *iters[meta.fileNum]
+		f := *iters[meta.FileNum]
 		return &f, nil, nil
 	}
 
@@ -47,10 +47,10 @@ func TestLevelIter(t *testing.T) {
 				iters = append(iters, f)
 
 				meta := fileMetadata{
-					fileNum: uint64(len(files)),
+					FileNum: uint64(len(files)),
 				}
-				meta.smallest = f.keys[0]
-				meta.largest = f.keys[len(f.keys)-1]
+				meta.Smallest = f.keys[0]
+				meta.Largest = f.keys[len(f.keys)-1]
 				files = append(files, meta)
 			}
 
@@ -129,7 +129,7 @@ func TestLevelIterBoundaries(t *testing.T) {
 	newIters := func(
 		meta *fileMetadata, _ *IterOptions, _ *uint64,
 	) (internalIterator, internalIterator, error) {
-		return readers[meta.fileNum].NewIter(nil /* lower */, nil /* upper */), nil, nil
+		return readers[meta.FileNum].NewIter(nil /* lower */, nil /* upper */), nil, nil
 	}
 
 	datadriven.RunTest(t, "testdata/level_iter_boundaries", func(d *datadriven.TestData) string {
@@ -193,14 +193,14 @@ func TestLevelIterBoundaries(t *testing.T) {
 			}
 			readers = append(readers, r)
 			files = append(files, fileMetadata{
-				fileNum:  fileNum,
-				smallest: meta.Smallest(cmp),
-				largest:  meta.Largest(cmp),
+				FileNum:  fileNum,
+				Smallest: meta.Smallest(cmp),
+				Largest:  meta.Largest(cmp),
 			})
 
 			var buf bytes.Buffer
 			for _, f := range files {
-				fmt.Fprintf(&buf, "%d: %s-%s\n", f.fileNum, f.smallest, f.largest)
+				fmt.Fprintf(&buf, "%d: %s-%s\n", f.FileNum, f.Smallest, f.Largest)
 			}
 			return buf.String()
 
@@ -274,10 +274,10 @@ func buildLevelIterTables(
 	for i := range readers {
 		iter := readers[i].NewIter(nil /* lower */, nil /* upper */)
 		key, _ := iter.First()
-		meta[i].fileNum = uint64(i)
-		meta[i].smallest = *key
+		meta[i].FileNum = uint64(i)
+		meta[i].Smallest = *key
 		key, _ = iter.Last()
-		meta[i].largest = *key
+		meta[i].Largest = *key
 	}
 	return readers, meta, keys
 }
@@ -295,7 +295,7 @@ func BenchmarkLevelIterSeekGE(b *testing.B) {
 							newIters := func(
 								meta *fileMetadata, _ *IterOptions, _ *uint64,
 							) (internalIterator, internalIterator, error) {
-								return readers[meta.fileNum].NewIter(nil /* lower */, nil /* upper */), nil, nil
+								return readers[meta.FileNum].NewIter(nil /* lower */, nil /* upper */), nil, nil
 							}
 							l := newLevelIter(nil, DefaultComparer.Compare, newIters, files, nil)
 							rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano())))
@@ -323,7 +323,7 @@ func BenchmarkLevelIterNext(b *testing.B) {
 							newIters := func(
 								meta *fileMetadata, _ *IterOptions, _ *uint64,
 							) (internalIterator, internalIterator, error) {
-								return readers[meta.fileNum].NewIter(nil /* lower */, nil /* upper */), nil, nil
+								return readers[meta.FileNum].NewIter(nil /* lower */, nil /* upper */), nil, nil
 							}
 							l := newLevelIter(nil, DefaultComparer.Compare, newIters, files, nil)
 
@@ -353,7 +353,7 @@ func BenchmarkLevelIterPrev(b *testing.B) {
 							newIters := func(
 								meta *fileMetadata, _ *IterOptions, _ *uint64,
 							) (internalIterator, internalIterator, error) {
-								return readers[meta.fileNum].NewIter(nil /* lower */, nil /* upper */), nil, nil
+								return readers[meta.FileNum].NewIter(nil /* lower */, nil /* upper */), nil, nil
 							}
 							l := newLevelIter(nil, DefaultComparer.Compare, newIters, files, nil)
 
diff --git a/table_cache_test.go b/table_cache_test.go
index 40cb55c..65d9b51 100644
--- a/table_cache_test.go
+++ b/table_cache_test.go
@@ -174,7 +174,7 @@ func testTableCacheRandomAccess(t *testing.T, concurrent bool) {
 			fileNum, sleepTime := rng.Intn(tableCacheTestNumTables), rng.Intn(1000)
 			rngMu.Unlock()
 			iter, _, err := c.newIters(
-				&fileMetadata{fileNum: uint64(fileNum)},
+				&fileMetadata{FileNum: uint64(fileNum)},
 				nil, /* iter options */
 				nil /* bytes iterated */)
 			if err != nil {
@@ -236,7 +236,7 @@ func TestTableCacheFrequentlyUsed(t *testing.T) {
 	for i := 0; i < N; i++ {
 		for _, j := range [...]int{pinned0, i % tableCacheTestNumTables, pinned1} {
 			iter, _, err := c.newIters(
-				&fileMetadata{fileNum: uint64(j)},
+				&fileMetadata{FileNum: uint64(j)},
 				nil, /* iter options */
 				nil /* bytes iterated */)
 			if err != nil {
@@ -274,7 +274,7 @@ func TestTableCacheEvictions(t *testing.T) {
 	for i := 0; i < N; i++ {
 		j := rng.Intn(tableCacheTestNumTables)
 		iter, _, err := c.newIters(
-			&fileMetadata{fileNum: uint64(j)},
+			&fileMetadata{FileNum: uint64(j)},
 			nil, /* iter options */
 			nil /* bytes iterated */)
 		if err != nil {
@@ -317,7 +317,7 @@ func TestTableCacheIterLeak(t *testing.T) {
 		t.Fatal(err)
 	}
 	if _, _, err := c.newIters(
-		&fileMetadata{fileNum: 0},
+		&fileMetadata{FileNum: 0},
 		nil, /* iter options */
 		nil /* bytes iterated */); err != nil {
 		t.Fatal(err)
diff --git a/tool/manifest_test.go b/tool/manifest_test.go
new file mode 100644
index 0000000..aa5633c
--- /dev/null
+++ b/tool/manifest_test.go
@@ -0,0 +1,13 @@
+// Copyright 2019 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 tool
+
+import (
+	"testing"
+)
+
+func TestManifest(t *testing.T) {
+	runTests(t, "testdata/manifest_*")
+}
diff --git a/tool/testdata/manifest_dump b/tool/testdata/manifest_dump
new file mode 100644
index 0000000..235bd7b
--- /dev/null
+++ b/tool/testdata/manifest_dump
@@ -0,0 +1,79 @@
+manifest dump
+----
+requires at least 1 arg(s), only received 0
+
+manifest dump
+../testdata/db-stage-2/MANIFEST-000001
+----
+../testdata/db-stage-2/MANIFEST-000001
+0
+  <empty>
+EOF
+
+manifest dump
+../testdata/db-stage-4/MANIFEST-000005
+----
+../testdata/db-stage-4/MANIFEST-000005
+0
+  comparer:     leveldb.BytewiseComparator
+35
+  <empty>
+44
+  log-num:      4
+  last-seq-num: 5
+  added:        L0 4:986[bar#5,0-foo#4,1]
+EOF
+--- L0 ---
+  4:986[bar#5,0-foo#4,1]
+--- L1 ---
+--- L2 ---
+--- L3 ---
+--- L4 ---
+--- L5 ---
+--- L6 ---
+
+manifest dump
+../testdata/db-stage-4/MANIFEST-000005
+--key=%x
+----
+../testdata/db-stage-4/MANIFEST-000005
+0
+  comparer:     leveldb.BytewiseComparator
+35
+  <empty>
+44
+  log-num:      4
+  last-seq-num: 5
+  added:        L0 4:986[626172#5,0-666f6f#4,1]
+EOF
+--- L0 ---
+  4:986[626172#5,0-666f6f#4,1]
+--- L1 ---
+--- L2 ---
+--- L3 ---
+--- L4 ---
+--- L5 ---
+--- L6 ---
+
+manifest dump
+../testdata/db-stage-4/MANIFEST-000005
+--key=null
+----
+../testdata/db-stage-4/MANIFEST-000005
+0
+  comparer:     leveldb.BytewiseComparator
+35
+  <empty>
+44
+  log-num:      4
+  last-seq-num: 5
+  added:        L0 4:986
+EOF
+--- L0 ---
+  4:986
+--- L1 ---
+--- L2 ---
+--- L3 ---
+--- L4 ---
+--- L5 ---
+--- L6 ---