Repository: cockroachdb/pebble
Pull request #4046 (2024-10-13 02:29:56)

sstable: fix colblk copyDataBlocks off-by-one
We would skip one data block if we exceeded the size of the buffer.

cockroach-teamcity: 

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


Patch:
diff --git a/cmd/pebble/db.go b/cmd/pebble/db.go
index 2565717c..6c05904d 100644
--- a/cmd/pebble/db.go
+++ b/cmd/pebble/db.go
@@ -61,6 +61,7 @@ func newPebbleDB(dir string) DB {
 		Comparer:                    &crdbtest.Comparer,
 		DisableWAL:                  disableWAL,
 		FormatMajorVersion:          pebble.FormatNewest,
+		KeySchema:                   crdbtest.KeySchema,
 		L0CompactionThreshold:       2,
 		L0StopWritesThreshold:       1000,
 		LBaseMaxBytes:               64 << 20, // 64 MB
diff --git a/compaction.go b/compaction.go
index 72f76be3..586e7356 100644
--- a/compaction.go
+++ b/compaction.go
@@ -2733,16 +2733,9 @@ func (d *DB) runCompaction(
 	}
 
 	// The table is typically written at the maximum allowable format implied by
-	// the current format major version of the DB.
-	tableFormat := d.FormatMajorVersion().MaxTableFormat()
-	// In format major versions with maximum table formats of Pebblev3, value
-	// blocks were conditional on an experimental setting. In format major
-	// versions with maximum table formats of Pebblev4 and higher, value blocks
-	// are always enabled.
-	if tableFormat == sstable.TableFormatPebblev3 &&
-		(d.opts.Experimental.EnableValueBlocks == nil || !d.opts.Experimental.EnableValueBlocks()) {
-		tableFormat = sstable.TableFormatPebblev2
-	}
+	// the current format major version of the DB, but Options may define
+	// additional constraints.
+	tableFormat := d.tableFormat()
 
 	// Release the d.mu lock while doing I/O.
 	// Note the unusual order: Unlock and then Lock.
@@ -3033,10 +3026,8 @@ func (d *DB) newCompactionOutput(
 			(cpuWorkHandle.Permitted() || d.opts.Experimental.ForceWriterParallelism)
 
 	// TODO(jackson): Make the compaction body generic over the RawWriter type,
-	// so that we don't need to pay the cost of dynamic dispatch. For now, we
-	// type assert into the only concrete RawWriter type we see (until colblk is
-	// integrated).
-	tw := sstable.NewRawWriter(writable, writerOpts).(*sstable.RawRowWriter)
+	// so that we don't need to pay the cost of dynamic dispatch?
+	tw := sstable.NewRawWriter(writable, writerOpts)
 	return objMeta, tw, cpuWorkHandle, nil
 }
 
diff --git a/format_major_version.go b/format_major_version.go
index dd62f32f..434cef71 100644
--- a/format_major_version.go
+++ b/format_major_version.go
@@ -191,6 +191,12 @@ const (
 	// as flushable ingested sstables.
 	FormatFlushableIngestExcises
 
+	// FormatColumnarBlocks is a format major version enabling use of the
+	// TableFormatPebblev5 table format, that encodes sstable data blocks, index
+	// blocks and keyspan blocks by organizing the KVs into columns within the
+	// block.
+	FormatColumnarBlocks
+
 	// TODO(msbutler): add major version for synthetic suffixes
 
 	// -- Add new versions here --
@@ -231,6 +237,8 @@ func (v FormatMajorVersion) MaxTableFormat() sstable.TableFormat {
 	case FormatDeleteSizedAndObsolete, FormatVirtualSSTables, FormatSyntheticPrefixSuffix,
 		FormatFlushableIngestExcises:
 		return sstable.TableFormatPebblev4
+	case FormatColumnarBlocks:
+		return sstable.TableFormatPebblev5
 	default:
 		panic(fmt.Sprintf("pebble: unsupported format major version: %s", v))
 	}
@@ -242,7 +250,7 @@ func (v FormatMajorVersion) MinTableFormat() sstable.TableFormat {
 	switch v {
 	case FormatDefault, FormatFlushableIngest, FormatPrePebblev1MarkedCompacted,
 		FormatDeleteSizedAndObsolete, FormatVirtualSSTables, FormatSyntheticPrefixSuffix,
-		FormatFlushableIngestExcises:
+		FormatFlushableIngestExcises, FormatColumnarBlocks:
 		return sstable.TableFormatPebblev1
 	default:
 		panic(fmt.Sprintf("pebble: unsupported format major version: %s", v))
@@ -282,6 +290,9 @@ var formatMajorVersionMigrations = map[FormatMajorVersion]func(*DB) error{
 	FormatFlushableIngestExcises: func(d *DB) error {
 		return d.finalizeFormatVersUpgrade(FormatFlushableIngestExcises)
 	},
+	FormatColumnarBlocks: func(d *DB) error {
+		return d.finalizeFormatVersUpgrade(FormatColumnarBlocks)
+	},
 }
 
 const formatVersionMarkerName = `format-version`
@@ -327,6 +338,28 @@ func (d *DB) FormatMajorVersion() FormatMajorVersion {
 	return FormatMajorVersion(d.mu.formatVers.vers.Load())
 }
 
+// tableFormat returns the TableFormat that new sstables should use.
+func (d *DB) tableFormat() sstable.TableFormat {
+	// The table is typically written at the maximum allowable format implied by
+	// the current format major version of the DB.
+	f := d.FormatMajorVersion().MaxTableFormat()
+	switch f {
+	case sstable.TableFormatPebblev3:
+		// In format major versions with maximum table formats of Pebblev3,
+		// value blocks were conditional on an experimental setting. In format
+		// major versions with maximum table formats of Pebblev4 and higher,
+		// value blocks are always enabled.
+		if d.opts.Experimental.EnableValueBlocks == nil || !d.opts.Experimental.EnableValueBlocks() {
+			f = sstable.TableFormatPebblev2
+		}
+	case sstable.TableFormatPebblev5:
+		if d.opts.Experimental.EnableColumnarBlocks == nil || !d.opts.Experimental.EnableColumnarBlocks() {
+			f = sstable.TableFormatPebblev4
+		}
+	}
+	return f
+}
+
 // RatchetFormatMajorVersion ratchets the opened database's format major
 // version to the provided version. It errors if the provided format
 // major version is below the database's current version. Once a
diff --git a/metamorphic/build.go b/metamorphic/build.go
index 7040a3c2..ce8d127a 100644
--- a/metamorphic/build.go
+++ b/metamorphic/build.go
@@ -252,9 +252,7 @@ func openExternalObj(
 ) {
 	objReader, objSize, err := t.externalStorage.ReadObject(context.Background(), externalObjName(externalObjID))
 	panicIfErr(err)
-	opts := sstable.ReaderOptions{
-		Comparer: t.opts.Comparer,
-	}
+	opts := t.opts.MakeReaderOptions()
 	reader, err = sstable.NewReader(
 		context.Background(),
 		objstorageprovider.NewRemoteReadable(objReader, objSize),
diff --git a/metamorphic/options.go b/metamorphic/options.go
index e5ab6c90..88a1c0c4 100644
--- a/metamorphic/options.go
+++ b/metamorphic/options.go
@@ -289,6 +289,7 @@ func defaultOptions() *pebble.Options {
 		}},
 		BlockPropertyCollectors: blockPropertyCollectorConstructors,
 	}
+	opts.Experimental.EnableColumnarBlocks = func() bool { return true }
 
 	// We don't want to run the level checker every time because it can slow down
 	// downloads and background compactions too much.
diff --git a/options.go b/options.go
index db4c9b9a..ca103796 100644
--- a/options.go
+++ b/options.go
@@ -26,6 +26,7 @@ import (
 	"github.com/cockroachdb/pebble/rangekey"
 	"github.com/cockroachdb/pebble/sstable"
 	"github.com/cockroachdb/pebble/sstable/block"
+	"github.com/cockroachdb/pebble/sstable/colblk"
 	"github.com/cockroachdb/pebble/vfs"
 	"github.com/cockroachdb/pebble/wal"
 )
@@ -687,6 +688,14 @@ type Options struct {
 		// for CPUWorkPermissionGranter for more details.
 		CPUWorkPermissionGranter CPUWorkPermissionGranter
 
+		// EnableColumnarBlocks is used to decide whether to enable writing
+		// TableFormatPebblev5 sstables. This setting is only respected by
+		// FormatColumnarBlocks. In lower format major versions, the
+		// TableFormatPebblev5 format is prohibited. If EnableColumnarBlocks is
+		// nil and the DB is at FormatColumnarBlocks, the DB defaults to not
+		// writing columnar blocks.
+		EnableColumnarBlocks func() bool
+
 		// EnableValueBlocks is used to decide whether to enable writing
 		// TableFormatPebblev3 sstables. This setting is only respected by a
 		// specific subset of format major versions: FormatSSTableValueBlocks,
@@ -881,6 +890,12 @@ type Options struct {
 	// The default value uses the underlying operating system's file system.
 	FS vfs.FS
 
+	// KeySchema defines the schema of a user key. When columnar blocks are in
+	// use (see FormatColumnarBlocks), the user may specify how a key should be
+	// decomposed into columns. If not set, colblk.DefaultKeySchema is used to
+	// construct a default key schema.
+	KeySchema colblk.KeySchema
+
 	// Lock, if set, must be a database lock acquired through LockDirectory for
 	// the same directory passed to Open. If provided, Open will skip locking
 	// the directory. Closing the database will not release the lock, and it's
@@ -1192,6 +1207,9 @@ func (o *Options) EnsureDefaults() *Options {
 	if o.Experimental.KeyValidationFunc == nil {
 		o.Experimental.KeyValidationFunc = func([]byte) error { return nil }
 	}
+	if len(o.KeySchema.ColumnTypes) == 0 {
+		o.KeySchema = colblk.DefaultKeySchema(o.Comparer, 16 /* bundleSize */)
+	}
 	if o.L0CompactionThreshold <= 0 {
 		o.L0CompactionThreshold = 4
 	}
@@ -1999,11 +2017,12 @@ func (o *Options) Validate() error {
 func (o *Options) MakeReaderOptions() sstable.ReaderOptions {
 	var readerOpts sstable.ReaderOptions
 	if o != nil {
-		readerOpts.LoadBlockSema = o.LoadBlockSema
 		readerOpts.Comparer = o.Comparer
-		readerOpts.Merger = o.Merger
 		readerOpts.Filters = o.Filters
+		readerOpts.KeySchema = o.KeySchema
+		readerOpts.LoadBlockSema = o.LoadBlockSema
 		readerOpts.LoggerAndTracer = o.LoggerAndTracer
+		readerOpts.Merger = o.Merger
 	}
 	return readerOpts
 }
@@ -2035,6 +2054,7 @@ func (o *Options) MakeWriterOptions(level int, format sstable.TableFormat) sstab
 	writerOpts.FilterPolicy = levelOpts.FilterPolicy
 	writerOpts.FilterType = levelOpts.FilterType
 	writerOpts.IndexBlockSize = levelOpts.IndexBlockSize
+	writerOpts.KeySchema = o.KeySchema
 	writerOpts.AllocatorSizeClasses = o.AllocatorSizeClasses
 	writerOpts.NumDeletionsThreshold = o.Experimental.NumDeletionsThreshold
 	writerOpts.DeletionSizeRatioThreshold = o.Experimental.DeletionSizeRatioThreshold
diff --git a/sstable/colblk_writer.go b/sstable/colblk_writer.go
index 6c6bc4f4..fade0223 100644
--- a/sstable/colblk_writer.go
+++ b/sstable/colblk_writer.go
@@ -127,9 +127,10 @@ func newColumnarWriter(writable objstorage.Writable, o WriterOptions) *RawColumn
 			blockSizeThreshold:      (o.IndexBlockSize*o.BlockSizeThreshold + 99) / 100,
 			sizeClassAwareThreshold: (o.IndexBlockSize*o.SizeClassAwareThreshold + 99) / 100,
 		},
-		allocatorSizeClasses: o.AllocatorSizeClasses,
-		opts:                 o,
-		layout:               makeLayoutWriter(writable, o),
+		allocatorSizeClasses:  o.AllocatorSizeClasses,
+		opts:                  o,
+		layout:                makeLayoutWriter(writable, o),
+		disableKeyOrderChecks: o.internal.DisableKeyOrderChecks,
 	}
 	w.dataBlock.Init(o.KeySchema)
 	w.indexBlock.Init()
@@ -1139,7 +1140,7 @@ func (w *RawColumnWriter) copyDataBlocks(
 	// blocks in one IO request as possible, while still utilizing the write queue in this
 	// writer.
 	lastBlockOffset := uint64(0)
-	for i := 0; i < len(blocks); i++ {
+	for i := 0; i < len(blocks); {
 		if blocks[i].bh.Offset < lastBlockOffset {
 			panic("pebble: copyDataBlocks called with blocks out of order")
 		}
diff --git a/tool/sstable.go b/tool/sstable.go
index f8bed160..86b2f2f0 100644
--- a/tool/sstable.go
+++ b/tool/sstable.go
@@ -148,9 +148,10 @@ func (s *sstableT) newReader(f vfs.File) (*sstable.Reader, error) {
 	}
 	o := sstable.ReaderOptions{
 		Comparer:  s.opts.Comparer,
+		Comparers: s.comparers,
+		KeySchema: s.opts.KeySchema,
 		Filters:   s.opts.Filters,
 		Mergers:   s.mergers,
-		Comparers: s.comparers,
 	}
 	c := pebble.NewCache(128 << 20 /* 128 MB */)
 	defer c.Unref()


Test patch:
diff --git a/checkpoint_test.go b/checkpoint_test.go
index 1c18098f..b688ac73 100644
--- a/checkpoint_test.go
+++ b/checkpoint_test.go
@@ -42,6 +42,7 @@ func testCheckpointImpl(t *testing.T, ddFile string, createOnShared bool) {
 		DisableAutomaticCompactions: true,
 		Logger:                      testLogger{t},
 	}
+	opts.Experimental.EnableColumnarBlocks = func() bool { return true }
 	opts.Experimental.RemoteStorage = remote.MakeSimpleFactory(map[remote.Locator]remote.Storage{
 		"": remoteMem,
 	})
diff --git a/compaction_test.go b/compaction_test.go
index 62a98361..16cba2eb 100644
--- a/compaction_test.go
+++ b/compaction_test.go
@@ -27,11 +27,13 @@ import (
 	"github.com/cockroachdb/pebble/internal/base"
 	"github.com/cockroachdb/pebble/internal/compact"
 	"github.com/cockroachdb/pebble/internal/manifest"
+	"github.com/cockroachdb/pebble/internal/testkeys"
 	"github.com/cockroachdb/pebble/internal/testutils"
 	"github.com/cockroachdb/pebble/objstorage"
 	"github.com/cockroachdb/pebble/objstorage/objstorageprovider"
 	"github.com/cockroachdb/pebble/objstorage/remote"
 	"github.com/cockroachdb/pebble/sstable"
+	"github.com/cockroachdb/pebble/sstable/colblk"
 	"github.com/cockroachdb/pebble/vfs"
 	"github.com/cockroachdb/pebble/vfs/errorfs"
 	"github.com/stretchr/testify/require"
@@ -612,10 +614,12 @@ func TestCompaction(t *testing.T) {
 
 	mem := vfs.NewMem()
 	opts := &Options{
-		FS:                    mem,
-		MemTableSize:          memTableSize,
+		Comparer:              testkeys.Comparer,
 		DebugCheck:            DebugCheckLevels,
+		FS:                    mem,
+		KeySchema:             colblk.DefaultKeySchema(testkeys.Comparer, 16),
 		L0CompactionThreshold: 8,
+		MemTableSize:          memTableSize,
 	}
 	opts.testingRandomized(t).WithFSDefaults()
 	d, err := Open("", opts)
@@ -657,7 +661,7 @@ func TestCompaction(t *testing.T) {
 				if err != nil {
 					return "", "", errors.WithStack(err)
 				}
-				r, err := sstable.NewReader(context.Background(), f, sstable.ReaderOptions{})
+				r, err := sstable.NewReader(context.Background(), f, opts.MakeReaderOptions())
 				if err != nil {
 					return "", "", errors.WithStack(err)
 				}
@@ -948,6 +952,7 @@ func TestManualCompaction(t *testing.T) {
 			EventListener:               compactionLogEventListener,
 			FormatMajorVersion:          randVersion(minVersion, maxVersion),
 		}).WithFSDefaults()
+		opts.Experimental.EnableColumnarBlocks = func() bool { return true }
 
 		var err error
 		d, err = Open("", opts)
@@ -1051,6 +1056,7 @@ func TestManualCompaction(t *testing.T) {
 					FormatMajorVersion:          randVersion(minVersion, maxVersion),
 					DisableAutomaticCompactions: true,
 				}).WithFSDefaults()
+				opts.Experimental.EnableColumnarBlocks = func() bool { return true }
 
 				var err error
 				if d, err = runDBDefineCmd(td, opts); err != nil {
@@ -1254,14 +1260,20 @@ func TestManualCompaction(t *testing.T) {
 		{
 			testData:   "testdata/manual_compaction_file_boundaries_delsized",
 			minVersion: FormatDeleteSizedAndObsolete,
+			maxVersion: FormatFlushableIngestExcises,
 		},
 		{
 			testData:   "testdata/manual_compaction_set_with_del_sstable_Pebblev4",
 			minVersion: FormatDeleteSizedAndObsolete,
+			maxVersion: FormatFlushableIngestExcises,
 		},
 		{
 			testData: "testdata/manual_compaction_multilevel",
 		},
+		{
+			testData:   "testdata/manual_compaction_set_with_del_sstable_Pebblev5",
+			minVersion: FormatColumnarBlocks,
+		},
 	}
 
 	for _, tc := range testCases {
@@ -1273,7 +1285,6 @@ func TestManualCompaction(t *testing.T) {
 			if maxVersion == 0 {
 				maxVersion = internalFormatNewest
 			}
-
 			runTest(t, tc.testData, minVersion, maxVersion, tc.verbose)
 		})
 	}
@@ -1348,7 +1359,7 @@ func TestCompactionDeleteOnlyHints(t *testing.T) {
 			},
 			FormatMajorVersion: internalFormatNewest,
 		}).WithFSDefaults()
-
+		opts.Experimental.EnableColumnarBlocks = func() bool { return true }
 		return opts, nil
 	}
 
@@ -1608,6 +1619,7 @@ func TestCompactionTombstones(t *testing.T) {
 					},
 					FormatMajorVersion: internalFormatNewest,
 				}).WithFSDefaults()
+				opts.Experimental.EnableColumnarBlocks = func() bool { return true }
 				var err error
 				d, err = runDBDefineCmd(td, opts)
 				if err != nil {
@@ -2444,6 +2456,7 @@ func TestMarkedForCompaction(t *testing.T) {
 			},
 		},
 	}).WithFSDefaults()
+	opts.Experimental.EnableColumnarBlocks = func() bool { return true }
 
 	reset := func() {
 		if d != nil {
diff --git a/event_listener_test.go b/event_listener_test.go
index f467262a..286f701c 100644
--- a/event_listener_test.go
+++ b/event_listener_test.go
@@ -64,6 +64,7 @@ func TestEventListener(t *testing.T) {
 				L0CompactionThreshold: 10,
 				WALDir:                "wal",
 			}
+			opts.Experimental.EnableColumnarBlocks = func() bool { return true }
 			var err error
 			d, err = Open("db", opts)
 			if err != nil {
diff --git a/external_iterator_test.go b/external_iterator_test.go
index d250d458..3b7012ad 100644
--- a/external_iterator_test.go
+++ b/external_iterator_test.go
@@ -20,8 +20,9 @@ import (
 func TestExternalIterator(t *testing.T) {
 	mem := vfs.NewMem()
 	o := &Options{
-		FS:       mem,
-		Comparer: testkeys.Comparer,
+		FS:                 mem,
+		FormatMajorVersion: internalFormatNewest,
+		Comparer:           testkeys.Comparer,
 	}
 	o.testingRandomized(t)
 	o.EnsureDefaults()
diff --git a/format_major_version_test.go b/format_major_version_test.go
index 364f3619..ba64dc55 100644
--- a/format_major_version_test.go
+++ b/format_major_version_test.go
@@ -28,8 +28,8 @@ func TestFormatMajorVersionStableValues(t *testing.T) {
 
 	// When we add a new version, we should add a check for the new version in
 	// addition to updating these expected values.
-	require.Equal(t, FormatNewest, FormatMajorVersion(18))
-	require.Equal(t, internalFormatNewest, FormatMajorVersion(18))
+	require.Equal(t, FormatNewest, FormatMajorVersion(19))
+	require.Equal(t, internalFormatNewest, FormatMajorVersion(19))
 }
 
 func TestFormatMajorVersion_MigrationDefined(t *testing.T) {
@@ -56,6 +56,8 @@ func TestRatchetFormat(t *testing.T) {
 	require.Equal(t, FormatSyntheticPrefixSuffix, d.FormatMajorVersion())
 	require.NoError(t, d.RatchetFormatMajorVersion(FormatFlushableIngestExcises))
 	require.Equal(t, FormatFlushableIngestExcises, d.FormatMajorVersion())
+	require.NoError(t, d.RatchetFormatMajorVersion(FormatColumnarBlocks))
+	require.Equal(t, FormatColumnarBlocks, d.FormatMajorVersion())
 
 	require.NoError(t, d.Close())
 
@@ -207,6 +209,7 @@ func TestFormatMajorVersions_TableFormat(t *testing.T) {
 		FormatVirtualSSTables:            {sstable.TableFormatPebblev1, sstable.TableFormatPebblev4},
 		FormatSyntheticPrefixSuffix:      {sstable.TableFormatPebblev1, sstable.TableFormatPebblev4},
 		FormatFlushableIngestExcises:     {sstable.TableFormatPebblev1, sstable.TableFormatPebblev4},
+		FormatColumnarBlocks:             {sstable.TableFormatPebblev1, sstable.TableFormatPebblev5},
 	}
 
 	// Valid versions.
diff --git a/ingest_test.go b/ingest_test.go
index 96811299..9d27adce 100644
--- a/ingest_test.go
+++ b/ingest_test.go
@@ -38,6 +38,7 @@ import (
 	"github.com/cockroachdb/pebble/record"
 	"github.com/cockroachdb/pebble/sstable"
 	"github.com/cockroachdb/pebble/sstable/block"
+	"github.com/cockroachdb/pebble/sstable/colblk"
 	"github.com/cockroachdb/pebble/vfs"
 	"github.com/cockroachdb/pebble/vfs/errorfs"
 	"github.com/kr/pretty"
@@ -217,8 +218,9 @@ func TestIngestLoadRand(t *testing.T) {
 	}
 
 	opts := (&Options{
-		Comparer: DefaultComparer,
-		FS:       mem,
+		Comparer:  base.DefaultComparer,
+		KeySchema: colblk.DefaultKeySchema(base.DefaultComparer, 16),
+		FS:        mem,
 	}).WithFSDefaults()
 	lr, err := ingestLoad(context.Background(), opts, version, paths, nil, nil, 0, pending)
 	require.NoError(t, err)
@@ -3627,7 +3629,7 @@ func TestIngestValidation(t *testing.T) {
 				require.NoError(t, err)
 				// Compute the layout of the sstable in order to find the
 				// appropriate block locations to corrupt.
-				r, err := sstable.NewReader(context.Background(), readable, sstable.ReaderOptions{})
+				r, err := sstable.NewReader(context.Background(), readable, opts.MakeReaderOptions())
 				require.NoError(t, err)
 				l, err := r.Layout()
 				require.NoError(t, err)
@@ -3666,8 +3668,10 @@ func TestIngestValidation(t *testing.T) {
 				defer func() { _ = fs.Remove(ingestTableName) }()
 
 				w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{
-					BlockSize:   blockSize,     // Create many smaller blocks.
+					BlockSize:   blockSize, // Create many smaller blocks.
+					Comparer:    opts.Comparer,
 					Compression: NoCompression, // For simpler debugging.
+					KeySchema:   opts.KeySchema,
 				})
 				for _, kv := range keyVals {
 					require.NoError(t, w.Set(kv.key, kv.val))
diff --git a/merging_iter_test.go b/merging_iter_test.go
index ec7c619d..2e719a39 100644
--- a/merging_iter_test.go
+++ b/merging_iter_test.go
@@ -256,7 +256,7 @@ func TestMergingIterDataDriven(t *testing.T) {
 					if err != nil {
 						return err.Error()
 					}
-					r, err := sstable.NewReader(context.Background(), readable, sstable.ReaderOptions{})
+					r, err := sstable.NewReader(context.Background(), readable, opts.MakeReaderOptions())
 					if err != nil {
 						return err.Error()
 					}
diff --git a/open_test.go b/open_test.go
index f72c3b93..a9e9f0d2 100644
--- a/open_test.go
+++ b/open_test.go
@@ -332,7 +332,7 @@ func TestNewDBFilenames(t *testing.T) {
 			"LOCK",
 			"MANIFEST-000001",
 			"OPTIONS-000003",
-			"marker.format-version.000005.018",
+			"marker.format-version.000006.019",
 			"marker.manifest.000001.MANIFEST-000001",
 		},
 	}
diff --git a/options_test.go b/options_test.go
index adb29344..f239b12e 100644
--- a/options_test.go
+++ b/options_test.go
@@ -34,6 +34,11 @@ func (o *Options) testingRandomized(t testing.TB) *Options {
 		o.FormatMajorVersion = FormatMinSupported + FormatMajorVersion(n)
 		t.Logf("Running %s with format major version %s", t.Name(), o.FormatMajorVersion.String())
 	}
+	// Enable columnar blocks if using a format major version that supports it.
+	if o.FormatMajorVersion >= FormatColumnarBlocks && o.Experimental.EnableColumnarBlocks == nil && rand.Int64N(4) > 0 {
+		o.Experimental.EnableColumnarBlocks = func() bool { return true }
+	}
+	o.EnsureDefaults()
 	return o
 }
 
diff --git a/testdata/checkpoint b/testdata/checkpoint
index 29156e02..c7cd5563 100644
--- a/testdata/checkpoint
+++ b/testdata/checkpoint
@@ -39,6 +39,10 @@ create: db/marker.format-version.000005.018
 close: db/marker.format-version.000005.018
 remove: db/marker.format-version.000004.017
 sync: db
+create: db/marker.format-version.000006.019
+close: db/marker.format-version.000006.019
+remove: db/marker.format-version.000005.018
+sync: db
 create: db/temporary.000003.dbtmp
 sync: db/temporary.000003.dbtmp
 close: db/temporary.000003.dbtmp
@@ -101,9 +105,9 @@ close: .
 open-dir: checkpoints/checkpoint1
 link: db/OPTIONS-000003 -> checkpoints/checkpoint1/OPTIONS-000003
 open-dir: checkpoints/checkpoint1
-create: checkpoints/checkpoint1/marker.format-version.000001.018
-sync-data: checkpoints/checkpoint1/marker.format-version.000001.018
-close: checkpoints/checkpoint1/marker.format-version.000001.018
+create: checkpoints/checkpoint1/marker.format-version.000001.019
+sync-data: checkpoints/checkpoint1/marker.format-version.000001.019
+close: checkpoints/checkpoint1/marker.format-version.000001.019
 sync: checkpoints/checkpoint1
 close: checkpoints/checkpoint1
 link: db/000005.sst -> checkpoints/checkpoint1/000005.sst
@@ -141,9 +145,9 @@ close: checkpoints
 open-dir: checkpoints/checkpoint2
 link: db/OPTIONS-000003 -> checkpoints/checkpoint2/OPTIONS-000003
 open-dir: checkpoints/checkpoint2
-create: checkpoints/checkpoint2/marker.format-version.000001.018
-sync-data: checkpoints/checkpoint2/marker.format-version.000001.018
-close: checkpoints/checkpoint2/marker.format-version.000001.018
+create: checkpoints/checkpoint2/marker.format-version.000001.019
+sync-data: checkpoints/checkpoint2/marker.format-version.000001.019
+close: checkpoints/checkpoint2/marker.format-version.000001.019
 sync: checkpoints/checkpoint2
 close: checkpoints/checkpoint2
 link: db/000007.sst -> checkpoints/checkpoint2/000007.sst
@@ -176,9 +180,9 @@ close: checkpoints
 open-dir: checkpoints/checkpoint3
 link: db/OPTIONS-000003 -> checkpoints/checkpoint3/OPTIONS-000003
 open-dir: checkpoints/checkpoint3
-create: checkpoints/checkpoint3/marker.format-version.000001.018
-sync-data: checkpoints/checkpoint3/marker.format-version.000001.018
-close: checkpoints/checkpoint3/marker.format-version.000001.018
+create: checkpoints/checkpoint3/marker.format-version.000001.019
+sync-data: checkpoints/checkpoint3/marker.format-version.000001.019
+close: checkpoints/checkpoint3/marker.format-version.000001.019
 sync: checkpoints/checkpoint3
 close: checkpoints/checkpoint3
 link: db/000005.sst -> checkpoints/checkpoint3/000005.sst
@@ -214,28 +218,28 @@ close: db/000009.sst
 sync: db
 sync: db/MANIFEST-000001
 open: db/000005.sst (options: *vfs.randomReadsOption)
-read-at(558, 53): db/000005.sst
-read-at(521, 37): db/000005.sst
-read-at(74, 447): db/000005.sst
+read-at(617, 53): db/000005.sst
+read-at(579, 38): db/000005.sst
+read-at(132, 447): db/000005.sst
 open: db/000009.sst (options: *vfs.randomReadsOption)
-read-at(553, 53): db/000009.sst
-read-at(516, 37): db/000009.sst
-read-at(69, 447): db/000009.sst
+read-at(621, 53): db/000009.sst
+read-at(583, 38): db/000009.sst
+read-at(136, 447): db/000009.sst
 open: db/000007.sst (options: *vfs.randomReadsOption)
-read-at(558, 53): db/000007.sst
-read-at(521, 37): db/000007.sst
-read-at(74, 447): db/000007.sst
-read-at(47, 27): db/000005.sst
+read-at(617, 53): db/000007.sst
+read-at(579, 38): db/000007.sst
+read-at(132, 447): db/000007.sst
+read-at(91, 41): db/000005.sst
 open: db/000005.sst (options: *vfs.sequentialReadsOption)
-read-at(0, 47): db/000005.sst
-read-at(47, 27): db/000007.sst
+read-at(0, 91): db/000005.sst
+read-at(91, 41): db/000007.sst
 open: db/000007.sst (options: *vfs.sequentialReadsOption)
-read-at(0, 47): db/000007.sst
+read-at(0, 91): db/000007.sst
 create: db/000010.sst
 close: db/000005.sst
-read-at(42, 27): db/000009.sst
+read-at(95, 41): db/000009.sst
 open: db/000009.sst (options: *vfs.sequentialReadsOption)
-read-at(0, 42): db/000009.sst
+read-at(0, 95): db/000009.sst
 close: db/000007.sst
 close: db/000009.sst
 sync-data: db/000010.sst
@@ -262,7 +266,7 @@ list db
 LOCK
 MANIFEST-000001
 OPTIONS-000003
-marker.format-version.000005.018
+marker.format-version.000006.019
 marker.manifest.000001.MANIFEST-000001
 
 list checkpoints/checkpoint1
@@ -272,7 +276,7 @@ list checkpoints/checkpoint1
 000007.sst
 MANIFEST-000001
 OPTIONS-000003
-marker.format-version.000001.018
+marker.format-version.000001.019
 marker.manifest.000001.MANIFEST-000001
 
 open checkpoints/checkpoint1 readonly
@@ -294,17 +298,17 @@ close: checkpoints/checkpoint1/000006.log
 scan checkpoints/checkpoint1
 ----
 open: checkpoints/checkpoint1/000007.sst (options: *vfs.randomReadsOption)
-read-at(558, 53): checkpoints/checkpoint1/000007.sst
-read-at(521, 37): checkpoints/checkpoint1/000007.sst
-read-at(74, 447): checkpoints/checkpoint1/000007.sst
-read-at(47, 27): checkpoints/checkpoint1/000007.sst
-read-at(0, 47): checkpoints/checkpoint1/000007.sst
+read-at(617, 53): checkpoints/checkpoint1/000007.sst
+read-at(579, 38): checkpoints/checkpoint1/000007.sst
+read-at(132, 447): checkpoints/checkpoint1/000007.sst
+read-at(91, 41): checkpoints/checkpoint1/000007.sst
+read-at(0, 91): checkpoints/checkpoint1/000007.sst
 open: checkpoints/checkpoint1/000005.sst (options: *vfs.randomReadsOption)
-read-at(558, 53): checkpoints/checkpoint1/000005.sst
-read-at(521, 37): checkpoints/checkpoint1/000005.sst
-read-at(74, 447): checkpoints/checkpoint1/000005.sst
-read-at(47, 27): checkpoints/checkpoint1/000005.sst
-read-at(0, 47): checkpoints/checkpoint1/000005.sst
+read-at(617, 53): checkpoints/checkpoint1/000005.sst
+read-at(579, 38): checkpoints/checkpoint1/000005.sst
+read-at(132, 447): checkpoints/checkpoint1/000005.sst
+read-at(91, 41): checkpoints/checkpoint1/000005.sst
+read-at(0, 91): checkpoints/checkpoint1/000005.sst
 a 1
 b 5
 c 3
@@ -317,11 +321,11 @@ g 10
 scan db
 ----
 open: db/000010.sst (options: *vfs.randomReadsOption)
-read-at(585, 53): db/000010.sst
-read-at(548, 37): db/000010.sst
-read-at(101, 447): db/000010.sst
-read-at(74, 27): db/000010.sst
-read-at(0, 74): db/000010.sst
+read-at(622, 53): db/000010.sst
+read-at(584, 38): db/000010.sst
+read-at(137, 447): db/000010.sst
+read-at(96, 41): db/000010.sst
+read-at(0, 96): db/000010.sst
 a 1
 b 5
 c 3
@@ -339,7 +343,7 @@ list checkpoints/checkpoint2
 000007.sst
 MANIFEST-000001
 OPTIONS-000003
-marker.format-version.000001.018
+marker.format-version.000001.019
 marker.manifest.000001.MANIFEST-000001
 
 open checkpoints/checkpoint2 readonly
@@ -361,11 +365,11 @@ close: checkpoints/checkpoint2/000006.log
 scan checkpoints/checkpoint2
 ----
 open: checkpoints/checkpoint2/000007.sst (options: *vfs.randomReadsOption)
-read-at(558, 53): checkpoints/checkpoint2/000007.sst
-read-at(521, 37): checkpoints/checkpoint2/000007.sst
-read-at(74, 447): checkpoints/checkpoint2/000007.sst
-read-at(47, 27): checkpoints/checkpoint2/000007.sst
-read-at(0, 47): checkpoints/checkpoint2/000007.sst
+read-at(617, 53): checkpoints/checkpoint2/000007.sst
+read-at(579, 38): checkpoints/checkpoint2/000007.sst
+read-at(132, 447): checkpoints/checkpoint2/000007.sst
+read-at(91, 41): checkpoints/checkpoint2/000007.sst
+read-at(0, 91): checkpoints/checkpoint2/000007.sst
 b 5
 d 7
 e 8
@@ -381,7 +385,7 @@ list checkpoints/checkpoint3
 000007.sst
 MANIFEST-000001
 OPTIONS-000003
-marker.format-version.000001.018
+marker.format-version.000001.019
 marker.manifest.000001.MANIFEST-000001
 
 open checkpoints/checkpoint3 readonly
@@ -403,17 +407,17 @@ close: checkpoints/checkpoint3/000006.log
 scan checkpoints/checkpoint3
 ----
 open: checkpoints/checkpoint3/000007.sst (options: *vfs.randomReadsOption)
-read-at(558, 53): checkpoints/checkpoint3/000007.sst
-read-at(521, 37): checkpoints/checkpoint3/000007.sst
-read-at(74, 447): checkpoints/checkpoint3/000007.sst
-read-at(47, 27): checkpoints/checkpoint3/000007.sst
-read-at(0, 47): checkpoints/checkpoint3/000007.sst
+read-at(617, 53): checkpoints/checkpoint3/000007.sst
+read-at(579, 38): checkpoints/checkpoint3/000007.sst
+read-at(132, 447): checkpoints/checkpoint3/000007.sst
+read-at(91, 41): checkpoints/checkpoint3/000007.sst
+read-at(0, 91): checkpoints/checkpoint3/000007.sst
 open: checkpoints/checkpoint3/000005.sst (options: *vfs.randomReadsOption)
-read-at(558, 53): checkpoints/checkpoint3/000005.sst
-read-at(521, 37): checkpoints/checkpoint3/000005.sst
-read-at(74, 447): checkpoints/checkpoint3/000005.sst
-read-at(47, 27): checkpoints/checkpoint3/000005.sst
-read-at(0, 47): checkpoints/checkpoint3/000005.sst
+read-at(617, 53): checkpoints/checkpoint3/000005.sst
+read-at(579, 38): checkpoints/checkpoint3/000005.sst
+read-at(132, 447): checkpoints/checkpoint3/000005.sst
+read-at(91, 41): checkpoints/checkpoint3/000005.sst
+read-at(0, 91): checkpoints/checkpoint3/000005.sst
 a 1
 b 5
 c 3
@@ -496,9 +500,9 @@ close: checkpoints
 open-dir: checkpoints/checkpoint4
 link: db/OPTIONS-000003 -> checkpoints/checkpoint4/OPTIONS-000003
 open-dir: checkpoints/checkpoint4
-create: checkpoints/checkpoint4/marker.format-version.000001.018
-sync-data: checkpoints/checkpoint4/marker.format-version.000001.018
-close: checkpoints/checkpoint4/marker.format-version.000001.018
+create: checkpoints/checkpoint4/marker.format-version.000001.019
+sync-data: checkpoints/checkpoint4/marker.format-version.000001.019
+close: checkpoints/checkpoint4/marker.format-version.000001.019
 sync: checkpoints/checkpoint4
 close: checkpoints/checkpoint4
 link: db/000010.sst -> checkpoints/checkpoint4/000010.sst
@@ -542,11 +546,11 @@ close: checkpoints/checkpoint4/000008.log
 scan checkpoints/checkpoint4
 ----
 open: checkpoints/checkpoint4/000010.sst (options: *vfs.randomReadsOption)
-read-at(585, 53): checkpoints/checkpoint4/000010.sst
-read-at(548, 37): checkpoints/checkpoint4/000010.sst
-read-at(101, 447): checkpoints/checkpoint4/000010.sst
-read-at(74, 27): checkpoints/checkpoint4/000010.sst
-read-at(0, 74): checkpoints/checkpoint4/000010.sst
+read-at(622, 53): checkpoints/checkpoint4/000010.sst
+read-at(584, 38): checkpoints/checkpoint4/000010.sst
+read-at(137, 447): checkpoints/checkpoint4/000010.sst
+read-at(96, 41): checkpoints/checkpoint4/000010.sst
+read-at(0, 96): checkpoints/checkpoint4/000010.sst
 a 1
 b 5
 d 7
@@ -586,7 +590,7 @@ list db
 LOCK
 MANIFEST-000001
 OPTIONS-000003
-marker.format-version.000005.018
+marker.format-version.000006.019
 marker.manifest.000001.MANIFEST-000001
 
 
@@ -601,9 +605,9 @@ close: checkpoints
 open-dir: checkpoints/checkpoint5
 link: db/OPTIONS-000003 -> checkpoints/checkpoint5/OPTIONS-000003
 open-dir: checkpoints/checkpoint5
-create: checkpoints/checkpoint5/marker.format-version.000001.018
-sync-data: checkpoints/checkpoint5/marker.format-version.000001.018
-close: checkpoints/checkpoint5/marker.format-version.000001.018
+create: checkpoints/checkpoint5/marker.format-version.000001.019
+sync-data: checkpoints/checkpoint5/marker.format-version.000001.019
+close: checkpoints/checkpoint5/marker.format-version.000001.019
 sync: checkpoints/checkpoint5
 close: checkpoints/checkpoint5
 link: db/000010.sst -> checkpoints/checkpoint5/000010.sst
@@ -699,9 +703,9 @@ close: checkpoints
 open-dir: checkpoints/checkpoint6
 link: db/OPTIONS-000003 -> checkpoints/checkpoint6/OPTIONS-000003
 open-dir: checkpoints/checkpoint6
-create: checkpoints/checkpoint6/marker.format-version.000001.018
-sync-data: checkpoints/checkpoint6/marker.format-version.000001.018
-close: checkpoints/checkpoint6/marker.format-version.000001.018
+create: checkpoints/checkpoint6/marker.format-version.000001.019
+sync-data: checkpoints/checkpoint6/marker.format-version.000001.019
+close: checkpoints/checkpoint6/marker.format-version.000001.019
 sync: checkpoints/checkpoint6
 close: checkpoints/checkpoint6
 link: db/000011.sst -> checkpoints/checkpoint6/000011.sst
diff --git a/testdata/checkpoint_shared b/testdata/checkpoint_shared
index 157e050a..9a0024e5 100644
--- a/testdata/checkpoint_shared
+++ b/testdata/checkpoint_shared
@@ -27,6 +27,10 @@ create: db/marker.format-version.000002.018
 close: db/marker.format-version.000002.018
 remove: db/marker.format-version.000001.017
 sync: db
+create: db/marker.format-version.000003.019
+close: db/marker.format-version.000003.019
+remove: db/marker.format-version.000002.018
+sync: db
 create: db/temporary.000003.dbtmp
 sync: db/temporary.000003.dbtmp
 close: db/temporary.000003.dbtmp
@@ -89,9 +93,9 @@ close: .
 open-dir: checkpoints/checkpoint1
 link: db/OPTIONS-000003 -> checkpoints/checkpoint1/OPTIONS-000003
 open-dir: checkpoints/checkpoint1
-create: checkpoints/checkpoint1/marker.format-version.000001.018
-sync-data: checkpoints/checkpoint1/marker.format-version.000001.018
-close: checkpoints/checkpoint1/marker.format-version.000001.018
+create: checkpoints/checkpoint1/marker.format-version.000001.019
+sync-data: checkpoints/checkpoint1/marker.format-version.000001.019
+close: checkpoints/checkpoint1/marker.format-version.000001.019
 sync: checkpoints/checkpoint1
 close: checkpoints/checkpoint1
 open: db/MANIFEST-000001 (options: *vfs.sequentialReadsOption)
@@ -138,9 +142,9 @@ close: checkpoints
 open-dir: checkpoints/checkpoint2
 link: db/OPTIONS-000003 -> checkpoints/checkpoint2/OPTIONS-000003
 open-dir: checkpoints/checkpoint2
-create: checkpoints/checkpoint2/marker.format-version.000001.018
-sync-data: checkpoints/checkpoint2/marker.format-version.000001.018
-close: checkpoints/checkpoint2/marker.format-version.000001.018
+create: checkpoints/checkpoint2/marker.format-version.000001.019
+sync-data: checkpoints/checkpoint2/marker.format-version.000001.019
+close: checkpoints/checkpoint2/marker.format-version.000001.019
 sync: checkpoints/checkpoint2
 close: checkpoints/checkpoint2
 open: db/MANIFEST-000001 (options: *vfs.sequentialReadsOption)
@@ -183,9 +187,9 @@ close: checkpoints
 open-dir: checkpoints/checkpoint3
 link: db/OPTIONS-000003 -> checkpoints/checkpoint3/OPTIONS-000003
 open-dir: checkpoints/checkpoint3
-create: checkpoints/checkpoint3/marker.format-version.000001.018
-sync-data: checkpoints/checkpoint3/marker.format-version.000001.018
-close: checkpoints/checkpoint3/marker.format-version.000001.018
+create: checkpoints/checkpoint3/marker.format-version.000001.019
+sync-data: checkpoints/checkpoint3/marker.format-version.000001.019
+close: checkpoints/checkpoint3/marker.format-version.000001.019
 sync: checkpoints/checkpoint3
 close: checkpoints/checkpoint3
 open: db/MANIFEST-000001 (options: *vfs.sequentialReadsOption)
@@ -242,7 +246,7 @@ LOCK
 MANIFEST-000001
 OPTIONS-000003
 REMOTE-OBJ-CATALOG-000001
-marker.format-version.000002.018
+marker.format-version.000003.019
 marker.manifest.000001.MANIFEST-000001
 marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001
 
@@ -252,7 +256,7 @@ list checkpoints/checkpoint1
 MANIFEST-000001
 OPTIONS-000003
 REMOTE-OBJ-CATALOG-000001
-marker.format-version.000001.018
+marker.format-version.000001.019
 marker.manifest.000001.MANIFEST-000001
 marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001
 
@@ -304,7 +308,7 @@ list checkpoints/checkpoint2
 MANIFEST-000001
 OPTIONS-000003
 REMOTE-OBJ-CATALOG-000001
-marker.format-version.000001.018
+marker.format-version.000001.019
 marker.manifest.000001.MANIFEST-000001
 marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001
 
diff --git a/testdata/compaction_delete_only_hints b/testdata/compaction_delete_only_hints
index e73600c1..809f22bc 100644
--- a/testdata/compaction_delete_only_hints
+++ b/testdata/compaction_delete_only_hints
@@ -88,7 +88,7 @@ maybe-compact
 Deletion hints:
   L0.000004 b-r seqnums(tombstone=200-230, file-smallest=30, type=point-key-only)
 Compactions:
-  [JOB 100] compacted(delete-only) multilevel L2 [000005] (605B) Score=0.00 + L3 [000006] (605B) Score=0.00 + L4 [000007] (605B) Score=0.00 -> L6 [000008] (41B), in 1.0s (2.0s total), output rate 41B/s
+  [JOB 100] compacted(delete-only) multilevel L2 [000005] (673B) Score=0.00 + L3 [000006] (673B) Score=0.00 + L4 [000007] (673B) Score=0.00 -> L6 [000008] (94B), in 1.0s (2.0s total), output rate 94B/s
 
 # Verify that compaction correctly handles the presence of multiple
 # overlapping hints which might delete a file multiple times. All of the
@@ -127,7 +127,7 @@ maybe-compact
 Deletion hints:
   L1.000005 b-r seqnums(tombstone=200-230, file-smallest=30, type=point-key-only)
 Compactions:
-  [JOB 100] compacted(delete-only) multilevel L2 [000005] (605B) Score=0.00 + L3 [000006] (605B) Score=0.00 + L4 [000007] (605B) Score=0.00 -> L6 [000008] (41B), in 1.0s (2.0s total), output rate 41B/s
+  [JOB 100] compacted(delete-only) multilevel L2 [000005] (673B) Score=0.00 + L3 [000006] (673B) Score=0.00 + L4 [000007] (673B) Score=0.00 -> L6 [000008] (94B), in 1.0s (2.0s total), output rate 94B/s
 
 # Test a range tombstone that is already compacted into L6.
 
@@ -206,7 +206,7 @@ maybe-compact
 Deletion hints:
   (none)
 Compactions:
-  [JOB 100] compacted(delete-only) multilevel L2 [000005] (605B) Score=0.00 + L3 [000006] (605B) Score=0.00 + L4 [000007] (605B) Score=0.00 -> L6 [000009] (41B), in 1.0s (2.0s total), output rate 41B/s
+  [JOB 100] compacted(delete-only) multilevel L2 [000005] (673B) Score=0.00 + L3 [000006] (673B) Score=0.00 + L4 [000007] (673B) Score=0.00 -> L6 [000009] (94B), in 1.0s (2.0s total), output rate 94B/s
 
 # A deletion hint present on an sstable in a higher level should NOT result in a
 # deletion-only compaction incorrectly removing an sstable in L6 following an
@@ -255,7 +255,7 @@ L0.000001 a-z seqnums(tombstone=5-27, file-smallest=0, type=point-key-only)
 close-snapshot
 10
 ----
-[JOB 100] compacted(elision-only) L6 [000004] (644B) Score=0.00 + L6 [] (0B) Score=0.00 -> L6 [000005] (591B), in 1.0s (2.0s total), output rate 591B/s
+[JOB 100] compacted(elision-only) L6 [000004] (749B) Score=0.00 + L6 [] (0B) Score=0.00 -> L6 [000005] (656B), in 1.0s (2.0s total), output rate 656B/s
 
 # In previous versions of the code, the deletion hint was removed by the
 # elision-only compaction because it zeroed sequence numbers of keys with
@@ -430,7 +430,7 @@ maybe-compact
 Deletion hints:
   (none)
 Compactions:
-  [JOB 100] compacted(delete-only) L6 [000006 000007 000008 000009 000011] (3.3KB) Score=0.00 -> L6 [] (0B), in 1.0s (2.0s total), output rate 0B/s
+  [JOB 100] compacted(delete-only) L6 [000006 000007 000008 000009 000011] (3.6KB) Score=0.00 -> L6 [] (0B), in 1.0s (2.0s total), output rate 0B/s
 
 # Verify that a delete-only compaction can partially excise a file.
 
@@ -474,7 +474,7 @@ maybe-compact
 Deletion hints:
   (none)
 Compactions:
-  [JOB 100] compacted(delete-only) multilevel L1 [000005] (650B) Score=0.00 + L2 [000006] (605B) Score=0.00 + L3 [000007] (605B) Score=0.00 + L4 [000008] (605B) Score=0.00 -> L6 [000009 000010] (42B), in 1.0s (2.0s total), output rate 42B/s
+  [JOB 100] compacted(delete-only) multilevel L1 [000005] (660B) Score=0.00 + L2 [000006] (673B) Score=0.00 + L3 [000007] (673B) Score=0.00 + L4 [000008] (673B) Score=0.00 -> L6 [000009 000010] (95B), in 1.0s (2.0s total), output rate 95B/s
 
 describe-lsm
 ----
@@ -542,7 +542,7 @@ maybe-compact
 Deletion hints:
   (none)
 Compactions:
-  [JOB 100] compacted(delete-only) L6 [000005] (597B) Score=0.00 -> L6 [] (0B), in 1.0s (2.0s total), output rate 0B/s
+  [JOB 100] compacted(delete-only) L6 [000005] (657B) Score=0.00 -> L6 [] (0B), in 1.0s (2.0s total), output rate 0B/s
 
 describe-lsm
 ----
@@ -608,7 +608,7 @@ maybe-compact
 Deletion hints:
   (none)
 Compactions:
-  [JOB 100] compacted(delete-only) L6 [000004] (760B) Score=0.00 -> L6 [000007 000008] (100B), in 1.0s (2.0s total), output rate 100B/s
+  [JOB 100] compacted(delete-only) L6 [000004] (851B) Score=0.00 -> L6 [000007 000008] (176B), in 1.0s (2.0s total), output rate 176B/s
 
 describe-lsm
 ----
diff --git a/testdata/compaction_picker_pick_file b/testdata/compaction_picker_pick_file
index 2e0edbe7..03abdcde 100644
--- a/testdata/compaction_picker_pick_file
+++ b/testdata/compaction_picker_pick_file
@@ -171,7 +171,7 @@ L5:
   000005:[f#11,SET-f#11,SET]: 57942 bytes (57KB)
 L6:
   000006:[c#0,SET-c#0,SET]: 66134 bytes (65KB)
-  000009:[d#13,SET-d#13,SET]: 589 bytes (589B)
+  000009:[d#13,SET-d#13,SET]: 654 bytes (654B)
   000007:[e#0,SET-e#0,SET]: 66134 bytes (65KB)
   000008:[f#0,SET-f#0,SET]: 66134 bytes (65KB)
 
@@ -209,8 +209,8 @@ L5:
   000011:[e#11,SET-e#11,SET]: 126 bytes (126B)
   000005:[f#11,SET-f#11,SET]: 57942 bytes (57KB)
 L6:
-  000012:[c#15,SET-c#15,SET]: 589 bytes (589B)
-  000009:[d#13,SET-d#13,SET]: 589 bytes (589B)
+  000012:[c#15,SET-c#15,SET]: 654 bytes (654B)
+  000009:[d#13,SET-d#13,SET]: 654 bytes (654B)
   000007:[e#0,SET-e#0,SET]: 66134 bytes (65KB)
   000008:[f#0,SET-f#0,SET]: 66134 bytes (65KB)
 
diff --git a/testdata/compaction_picker_scores b/testdata/compaction_picker_scores
index b3cd4a06..f0cb04d5 100644
--- a/testdata/compaction_picker_scores
+++ b/testdata/compaction_picker_scores
@@ -145,7 +145,7 @@ wait-pending-table-stats
 num-entries: 5
 num-deletions: 5
 num-range-key-sets: 0
-point-deletions-bytes-estimate: 164517
+point-deletions-bytes-estimate: 164552
 range-deletions-bytes-estimate: 0
 
 maybe-compact
diff --git a/testdata/compaction_tombstones b/testdata/compaction_tombstones
index 282db9e1..a0b07e4c 100644
--- a/testdata/compaction_tombstones
+++ b/testdata/compaction_tombstones
@@ -41,7 +41,7 @@ range-deletions-bytes-estimate: 0
 
 maybe-compact
 ----
-[JOB 100] compacted(elision-only) L6 [000004] (650B) Score=0.00 + L6 [] (0B) Score=0.00 -> L6 [] (0B), in 1.0s (2.0s total), output rate 0B/s
+[JOB 100] compacted(elision-only) L6 [000004] (660B) Score=0.00 + L6 [] (0B) Score=0.00 -> L6 [] (0B), in 1.0s (2.0s total), output rate 0B/s
 
 # Test a table that straddles a snapshot. It should not be compacted.
 define snapshots=(50) auto-compactions=off
@@ -80,12 +80,12 @@ wait-pending-table-stats
 num-entries: 2
 num-deletions: 1
 num-range-key-sets: 0
-point-deletions-bytes-estimate: 84
+point-deletions-bytes-estimate: 93
 range-deletions-bytes-estimate: 0
 
 maybe-compact
 ----
-[JOB 100] compacted(elision-only) L6 [000004] (642B) Score=0.00 + L6 [] (0B) Score=0.00 -> L6 [000005] (591B), in 1.0s (2.0s total), output rate 591B/s
+[JOB 100] compacted(elision-only) L6 [000004] (711B) Score=0.00 + L6 [] (0B) Score=0.00 -> L6 [000005] (656B), in 1.0s (2.0s total), output rate 656B/s
 
 version
 ----
@@ -119,8 +119,8 @@ wait-pending-table-stats
 num-entries: 6
 num-deletions: 2
 num-range-key-sets: 0
-point-deletions-bytes-estimate: 38
-range-deletions-bytes-estimate: 66
+point-deletions-bytes-estimate: 46
+range-deletions-bytes-estimate: 101
 
 maybe-compact
 ----
@@ -134,7 +134,7 @@ close-snapshot
 close-snapshot
 103
 ----
-[JOB 100] compacted(elision-only) L6 [000004] (794B) Score=0.00 + L6 [] (0B) Score=0.00 -> L6 [] (0B), in 1.0s (2.0s total), output rate 0B/s
+[JOB 100] compacted(elision-only) L6 [000004] (881B) Score=0.00 + L6 [] (0B) Score=0.00 -> L6 [] (0B), in 1.0s (2.0s total), output rate 0B/s
 
 # Test a table that contains both deletions and non-deletions, but whose
 # non-deletions well outnumber its deletions. The table should not be
@@ -152,7 +152,7 @@ wait-pending-table-stats
 num-entries: 11
 num-deletions: 1
 num-range-key-sets: 0
-point-deletions-bytes-estimate: 21
+point-deletions-bytes-estimate: 22
 range-deletions-bytes-estimate: 0
 
 close-snapshot
@@ -197,7 +197,7 @@ num-entries: 5
 num-deletions: 1
 num-range-key-sets: 0
 point-deletions-bytes-estimate: 0
-range-deletions-bytes-estimate: 16492
+range-deletions-bytes-estimate: 16824
 
 # Because we set max bytes low, maybe-compact will trigger an automatic
 # compaction in preference over an elision-only compaction.
@@ -208,7 +208,7 @@ range-deletions-bytes-estimate: 16492
 
 maybe-compact
 ----
-[JOB 100] compacted(default) L5 [000004 000005] (25KB) Score=87.29 + L6 [000007] (17KB) Score=0.73 -> L6 [000009] (25KB), in 1.0s (2.0s total), output rate 25KB/s
+[JOB 100] compacted(default) L5 [000004 000005] (26KB) Score=88.36 + L6 [000007] (17KB) Score=0.73 -> L6 [000009] (25KB), in 1.0s (2.0s total), output rate 25KB/s
 
 define level-max-bytes=(L5 : 1000) auto-compactions=off
 L5
@@ -233,7 +233,7 @@ wait-pending-table-stats
 num-entries: 3
 num-deletions: 3
 num-range-key-sets: 0
-point-deletions-bytes-estimate: 6786
+point-deletions-bytes-estimate: 6917
 range-deletions-bytes-estimate: 0
 
 # By plain file size, 000005 should be picked because it is larger and
@@ -243,7 +243,7 @@ range-deletions-bytes-estimate: 0
 
 maybe-compact
 ----
-[JOB 100] compacted(default) L5 [000004] (652B) Score=13.17 + L6 [000006] (13KB) Score=0.92 -> L6 [] (0B), in 1.0s (2.0s total), output rate 0B/s
+[JOB 100] compacted(default) L5 [000004] (715B) Score=13.49 + L6 [000006] (13KB) Score=0.92 -> L6 [] (0B), in 1.0s (2.0s total), output rate 0B/s
 
 # A table containing only range keys is not eligible for elision.
 # RANGEKEYDEL or RANGEKEYUNSET.
@@ -319,11 +319,11 @@ num-entries: 3
 num-deletions: 1
 num-range-key-sets: 0
 point-deletions-bytes-estimate: 0
-range-deletions-bytes-estimate: 41
+range-deletions-bytes-estimate: 94
 
 maybe-compact
 ----
-[JOB 100] compacted(elision-only) L6 [000004] (796B) Score=0.00 + L6 [] (0B) Score=0.00 -> L6 [000005] (597B), in 1.0s (2.0s total), output rate 597B/s
+[JOB 100] compacted(elision-only) L6 [000004] (938B) Score=0.00 + L6 [] (0B) Score=0.00 -> L6 [000005] (657B), in 1.0s (2.0s total), output rate 657B/s
 
 # Close the DB, asserting that the reference counts balance.
 close
@@ -359,7 +359,7 @@ wait-pending-table-stats
 num-entries: 2
 num-deletions: 1
 num-range-key-sets: 0
-point-deletions-bytes-estimate: 2713
+point-deletions-bytes-estimate: 2766
 range-deletions-bytes-estimate: 0
 
 wait-pending-table-stats
@@ -369,11 +369,11 @@ num-entries: 2
 num-deletions: 1
 num-range-key-sets: 0
 point-deletions-bytes-estimate: 0
-range-deletions-bytes-estimate: 8246
+range-deletions-bytes-estimate: 8380
 
 maybe-compact
 ----
-[JOB 100] compacted(delete-only) L6 [000007] (13KB) Score=0.00 -> L6 [000008] (8.1KB), in 1.0s (2.0s total), output rate 8.1KB/s
+[JOB 100] compacted(delete-only) L6 [000007] (13KB) Score=0.00 -> L6 [000008] (8.2KB), in 1.0s (2.0s total), output rate 8.2KB/s
 
 # The same LSM as above. However, this time, with point tombstone weighting at
 # 2x, the table with the point tombstone (000004) will be selected as the
@@ -402,7 +402,7 @@ wait-pending-table-stats
 num-entries: 2
 num-deletions: 1
 num-range-key-sets: 0
-point-deletions-bytes-estimate: 2713
+point-deletions-bytes-estimate: 2766
 range-deletions-bytes-estimate: 0
 
 wait-pending-table-stats
@@ -412,8 +412,8 @@ num-entries: 2
 num-deletions: 1
 num-range-key-sets: 0
 point-deletions-bytes-estimate: 0
-range-deletions-bytes-estimate: 8246
+range-deletions-bytes-estimate: 8380
 
 maybe-compact
 ----
-[JOB 100] compacted(delete-only) L6 [000007] (13KB) Score=0.00 -> L6 [000008] (8.1KB), in 1.0s (2.0s total), output rate 8.1KB/s
+[JOB 100] compacted(delete-only) L6 [000007] (13KB) Score=0.00 -> L6 [000008] (8.2KB), in 1.0s (2.0s total), output rate 8.2KB/s
diff --git a/testdata/event_listener b/testdata/event_listener
index 6bc9b66f..529fd577 100644
--- a/testdata/event_listener
+++ b/testdata/event_listener
@@ -51,6 +51,11 @@ close: db/marker.format-version.000005.018
 remove: db/marker.format-version.000004.017
 sync: db
 upgraded to format version: 018
+create: db/marker.format-version.000006.019
+close: db/marker.format-version.000006.019
+remove: db/marker.format-version.000005.018
+sync: db
+upgraded to format version: 019
 create: db/temporary.000003.dbtmp
 sync: db/temporary.000003.dbtmp
 close: db/temporary.000003.dbtmp
@@ -79,7 +84,7 @@ close: db/marker.manifest.000002.MANIFEST-000006
 remove: db/marker.manifest.000001.MANIFEST-000001
 sync: db
 [JOB 3] MANIFEST created 000006
-[JOB 3] flushed 1 memtable (100B) to L0 [000005] (590B), in 1.0s (2.0s total), output rate 590B/s
+[JOB 3] flushed 1 memtable (100B) to L0 [000005] (656B), in 1.0s (2.0s total), output rate 656B/s
 
 compact
 ----
@@ -103,24 +108,24 @@ close: db/marker.manifest.000003.MANIFEST-000009
 remove: db/marker.manifest.000002.MANIFEST-000006
 sync: db
 [JOB 5] MANIFEST created 000009
-[JOB 5] flushed 1 memtable (100B) to L0 [000008] (590B), in 1.0s (2.0s total), output rate 590B/s
+[JOB 5] flushed 1 memtable (100B) to L0 [000008] (656B), in 1.0s (2.0s total), output rate 656B/s
 remove: db/MANIFEST-000001
 [JOB 5] MANIFEST deleted 000001
-[JOB 6] compacting(default) L0 [000005 000008] (1.2KB) Score=0.00 + L6 [] (0B) Score=0.00; OverlappingRatio: Single 0.00, Multi 0.00
+[JOB 6] compacting(default) L0 [000005 000008] (1.3KB) Score=0.00 + L6 [] (0B) Score=0.00; OverlappingRatio: Single 0.00, Multi 0.00
 open: db/000005.sst (options: *vfs.randomReadsOption)
-read-at(537, 53): db/000005.sst
-read-at(500, 37): db/000005.sst
-read-at(53, 447): db/000005.sst
+read-at(603, 53): db/000005.sst
+read-at(566, 37): db/000005.sst
+read-at(119, 447): db/000005.sst
 open: db/000008.sst (options: *vfs.randomReadsOption)
-read-at(537, 53): db/000008.sst
-read-at(500, 37): db/000008.sst
-read-at(53, 447): db/000008.sst
-read-at(26, 27): db/000005.sst
+read-at(603, 53): db/000008.sst
+read-at(566, 37): db/000008.sst
+read-at(119, 447): db/000008.sst
+read-at(78, 41): db/000005.sst
 open: db/000005.sst (options: *vfs.sequentialReadsOption)
-read-at(0, 26): db/000005.sst
-read-at(26, 27): db/000008.sst
+read-at(0, 78): db/000005.sst
+read-at(78, 41): db/000008.sst
 open: db/000008.sst (options: *vfs.sequentialReadsOption)
-read-at(0, 26): db/000008.sst
+read-at(0, 78): db/000008.sst
 close: db/000008.sst
 close: db/000005.sst
 create: db/000010.sst
@@ -136,7 +141,7 @@ close: db/marker.manifest.000004.MANIFEST-000011
 remove: db/marker.manifest.000003.MANIFEST-000009
 sync: db
 [JOB 6] MANIFEST created 000011
-[JOB 6] compacted(default) L0 [000005 000008] (1.2KB) Score=0.00 + L6 [] (0B) Score=0.00 -> L6 [000010] (590B), in 1.0s (3.0s total), output rate 590B/s
+[JOB 6] compacted(default) L0 [000005 000008] (1.3KB) Score=0.00 + L6 [] (0B) Score=0.00 -> L6 [000010] (653B), in 1.0s (3.0s total), output rate 653B/s
 close: db/000005.sst
 close: db/000008.sst
 remove: db/000005.sst
@@ -171,7 +176,7 @@ close: db/marker.manifest.000005.MANIFEST-000014
 remove: db/marker.manifest.000004.MANIFEST-000011
 sync: db
 [JOB 8] MANIFEST created 000014
-[JOB 8] flushed 1 memtable (100B) to L0 [000013] (590B), in 1.0s (2.0s total), output rate 590B/s
+[JOB 8] flushed 1 memtable (100B) to L0 [000013] (656B), in 1.0s (2.0s total), output rate 656B/s
 
 enable-file-deletions
 ----
@@ -181,11 +186,11 @@ remove: db/MANIFEST-000009
 ingest
 ----
 open: ext/0
-read-at(537, 53): ext/0
-read-at(500, 37): ext/0
-read-at(53, 447): ext/0
-read-at(26, 27): ext/0
-read-at(0, 26): ext/0
+read-at(600, 53): ext/0
+read-at(563, 37): ext/0
+read-at(116, 447): ext/0
+read-at(75, 41): ext/0
+read-at(0, 75): ext/0
 close: ext/0
 link: ext/0 -> db/000015.sst
 [JOB 10] ingesting: sstable created 000015
@@ -201,33 +206,33 @@ sync: db
 remove: db/MANIFEST-000011
 [JOB 10] MANIFEST deleted 000011
 remove: ext/0
-[JOB 10] ingested L0:000015 (590B)
+[JOB 10] ingested L0:000015 (653B)
 
 metrics
 ----
       |                             |       |       |   ingested   |     moved    |    written   |       |    amp
 level | tables  size val-bl vtables | score |   in  | tables  size | tables  size | tables  size |  read |   r   w
 ------+-----------------------------+-------+-------+--------------+--------------+--------------+-------+---------
-    0 |     2  1.2KB     0B       0 |  0.40 |   81B |     1   590B |     0     0B |     3  1.7KB |    0B |   2 21.9
+    0 |     2  1.3KB     0B       0 |  0.40 |   81B |     1   653B |     0     0B |     3  1.9KB |    0B |   2 24.3
     1 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     2 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     3 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     4 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     5 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
-    6 |     1   590B     0B       0 |     - | 1.2KB |     0     0B |     0     0B |     1   590B | 1.2KB |   1  0.5
-total |     3  1.7KB     0B       0 |     - |  671B |     1   590B |     0     0B |     4  3.0KB | 1.2KB |   3  4.5
+    6 |     1   653B     0B       0 |     - | 1.3KB |     0     0B |     0     0B |     1   653B | 1.3KB |   1  0.5
+total |     3  1.9KB     0B       0 |     - |  734B |     1   653B |     0     0B |     4  3.3KB | 1.3KB |   3  4.6
 -------------------------------------------------------------------------------------------------------------------
 WAL: 1 files (0B)  in: 48B  written: 81B (69% overhead)
 Flushes: 3
-Compactions: 1  estimated debt: 1.7KB  in progress: 0 (0B)
+Compactions: 1  estimated debt: 1.9KB  in progress: 0 (0B)
              default: 1  delete: 0  elision: 0  move: 0  read: 0  tombstone-density: 0  rewrite: 0  copy: 0  multi-level: 0
 MemTables: 1 (256KB)  zombie: 1 (256KB)
 Zombie tables: 0 (0B, local: 0B)
 Backing tables: 0 (0B)
 Virtual tables: 0 (0B)
-Local tables size: 1.7KB
+Local tables size: 1.9KB
 Compression types: snappy: 3
-Block cache: 2 entries (43B)  hit rate: 0.0%
+Block cache: 2 entries (120B)  hit rate: 0.0%
 Table cache: 0 entries (0B)  hit rate: 50.0%
 Secondary cache: 0 entries (0B)  hit rate: 0.0%
 Snapshots: 0  earliest seq num: 0
@@ -244,18 +249,18 @@ ingest-flushable
 ----
 sync-data: wal/000012.log
 open: ext/a
-read-at(537, 53): ext/a
-read-at(500, 37): ext/a
-read-at(53, 447): ext/a
-read-at(26, 27): ext/a
-read-at(0, 26): ext/a
+read-at(600, 53): ext/a
+read-at(563, 37): ext/a
+read-at(116, 447): ext/a
+read-at(75, 41): ext/a
+read-at(0, 75): ext/a
 close: ext/a
 open: ext/b
-read-at(537, 53): ext/b
-read-at(500, 37): ext/b
-read-at(53, 447): ext/b
-read-at(26, 27): ext/b
-read-at(0, 26): ext/b
+read-at(600, 53): ext/b
+read-at(563, 37): ext/b
+read-at(116, 447): ext/b
+read-at(75, 41): ext/b
+read-at(0, 75): ext/b
 close: ext/b
 link: ext/a -> db/000017.sst
 [JOB 11] ingesting: sstable created 000017
@@ -275,7 +280,7 @@ sync: wal
 [JOB 13] WAL created 000020
 remove: ext/a
 remove: ext/b
-[JOB 11] ingested as flushable 000017 (590B), 000018 (590B)
+[JOB 11] ingested as flushable 000017 (653B), 000018 (653B)
 sync-data: wal/000020.log
 close: wal/000020.log
 create: wal/000021.log
@@ -288,7 +293,7 @@ sync-data: db/000022.sst
 close: db/000022.sst
 sync: db
 sync: db/MANIFEST-000016
-[JOB 15] flushed 1 memtable (100B) to L0 [000022] (590B), in 1.0s (2.0s total), output rate 590B/s
+[JOB 15] flushed 1 memtable (100B) to L0 [000022] (656B), in 1.0s (2.0s total), output rate 656B/s
 [JOB 16] flushing 2 ingested tables
 create: db/MANIFEST-000023
 close: db/MANIFEST-000016
@@ -298,7 +303,7 @@ close: db/marker.manifest.000007.MANIFEST-000023
 remove: db/marker.manifest.000006.MANIFEST-000016
 sync: db
 [JOB 16] MANIFEST created 000023
-[JOB 16] flushed 2 ingested flushables L0:000017 (590B) + L6:000018 (590B) in 1.0s (2.0s total), output rate 1.2KB/s
+[JOB 16] flushed 2 ingested flushables L0:000017 (653B) + L6:000018 (653B) in 1.0s (2.0s total), output rate 1.3KB/s
 remove: db/MANIFEST-000014
 [JOB 16] MANIFEST deleted 000014
 [JOB 17] flushing 1 memtable (100B) to L0
@@ -310,32 +315,32 @@ metrics
       |                             |       |       |   ingested   |     moved    |    written   |       |    amp
 level | tables  size val-bl vtables | score |   in  | tables  size | tables  size | tables  size |  read |   r   w
 ------+-----------------------------+-------+-------+--------------+--------------+--------------+-------+---------
-    0 |     4  2.3KB     0B       0 |  0.80 |  108B |     2  1.2KB |     0     0B |     4  2.3KB |    0B |   4 21.9
+    0 |     4  2.6KB     0B       0 |  0.80 |  108B |     2  1.3KB |     0     0B |     4  2.6KB |    0B |   4 24.3
     1 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     2 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     3 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     4 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     5 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
-    6 |     2  1.2KB     0B       0 |     - | 1.2KB |     1   590B |     0     0B |     1   590B | 1.2KB |   1  0.5
-total |     6  3.5KB     0B       0 |     - | 1.8KB |     3  1.7KB |     0     0B |     5  4.7KB | 1.2KB |   5  2.6
+    6 |     2  1.3KB     0B       0 |     - | 1.3KB |     1   653B |     0     0B |     1   653B | 1.3KB |   1  0.5
+total |     6  3.8KB     0B       0 |     - | 2.0KB |     3  1.9KB |     0     0B |     5  5.2KB | 1.3KB |   5  2.6
 -------------------------------------------------------------------------------------------------------------------
 WAL: 1 files (0B)  in: 82B  written: 108B (32% overhead)
 Flushes: 6
-Compactions: 1  estimated debt: 3.5KB  in progress: 0 (0B)
+Compactions: 1  estimated debt: 3.8KB  in progress: 0 (0B)
              default: 1  delete: 0  elision: 0  move: 0  read: 0  tombstone-density: 0  rewrite: 0  copy: 0  multi-level: 0
 MemTables: 1 (512KB)  zombie: 1 (512KB)
 Zombie tables: 0 (0B, local: 0B)
 Backing tables: 0 (0B)
 Virtual tables: 0 (0B)
-Local tables size: 3.5KB
+Local tables size: 3.8KB
 Compression types: snappy: 6
-Block cache: 6 entries (129B)  hit rate: 0.0%
+Block cache: 6 entries (360B)  hit rate: 0.0%
 Table cache: 0 entries (0B)  hit rate: 50.0%
 Secondary cache: 0 entries (0B)  hit rate: 0.0%
 Snapshots: 0  earliest seq num: 0
 Table iters: 0
 Filter utility: 0.0%
-Ingestions: 2  as flushable: 1 (1.2KB in 2 tables)
+Ingestions: 2  as flushable: 1 (1.3KB in 2 tables)
 Cgo memory usage: 0B  block cache: 0B (data: 0B, maps: 0B, entries: 0B)  memtables: 0B
 
 sstables
@@ -358,9 +363,9 @@ close: .
 open-dir: checkpoint
 link: db/OPTIONS-000003 -> checkpoint/OPTIONS-000003
 open-dir: checkpoint
-create: checkpoint/marker.format-version.000001.018
-sync-data: checkpoint/marker.format-version.000001.018
-close: checkpoint/marker.format-version.000001.018
+create: checkpoint/marker.format-version.000001.019
+sync-data: checkpoint/marker.format-version.000001.019
+close: checkpoint/marker.format-version.000001.019
 sync: checkpoint
 close: checkpoint
 link: db/000013.sst -> checkpoint/000013.sst
diff --git a/testdata/external_iterator b/testdata/external_iterator
index 87664996..ad900e87 100644
--- a/testdata/external_iterator
+++ b/testdata/external_iterator
@@ -245,4 +245,4 @@ aaaa@3: (aaaa@3, .)
 aaaa@1: (aaaa@1, .)
 aaaaa@3: (aaaaa@3, .)
 aaaaa@1: (aaaaa@1, .)
-stats: seeked 5 times (5 internal); stepped 5 times (5 internal); blocks: 0B cached, 547B not cached (read time: 0s); points: 10 (50B keys, 35B values); separated: 15 (65B, 25B fetched)
+stats: seeked 5 times (5 internal); stepped 5 times (5 internal); blocks: 0B cached, 1.3KB not cached (read time: 0s); points: 10 (50B keys, 35B values); separated: 5 (25B, 25B fetched)
diff --git a/testdata/flushable_ingest b/testdata/flushable_ingest
index 731538b3..81b5cad5 100644
--- a/testdata/flushable_ingest
+++ b/testdata/flushable_ingest
@@ -60,7 +60,7 @@ LOCK
 MANIFEST-000001
 OPTIONS-000003
 ext
-marker.format-version.000005.018
+marker.format-version.000006.019
 marker.manifest.000001.MANIFEST-000001
 
 # Test basic WAL replay
@@ -81,7 +81,7 @@ LOCK
 MANIFEST-000001
 OPTIONS-000003
 ext
-marker.format-version.000005.018
+marker.format-version.000006.019
 marker.manifest.000001.MANIFEST-000001
 
 open
@@ -390,7 +390,7 @@ LOCK
 MANIFEST-000001
 OPTIONS-000003
 ext
-marker.format-version.000005.018
+marker.format-version.000006.019
 marker.manifest.000001.MANIFEST-000001
 
 close
@@ -410,7 +410,7 @@ LOCK
 MANIFEST-000001
 OPTIONS-000003
 ext
-marker.format-version.000005.018
+marker.format-version.000006.019
 marker.manifest.000001.MANIFEST-000001
 
 open
@@ -443,7 +443,7 @@ MANIFEST-000001
 MANIFEST-000011
 OPTIONS-000014
 ext
-marker.format-version.000005.018
+marker.format-version.000006.019
 marker.manifest.000002.MANIFEST-000011
 
 # Make sure that the new mutable memtable can accept writes.
@@ -586,7 +586,7 @@ LOCK
 MANIFEST-000001
 OPTIONS-000003
 ext
-marker.format-version.000005.018
+marker.format-version.000006.019
 marker.manifest.000001.MANIFEST-000001
 
 close
@@ -605,7 +605,7 @@ MANIFEST-000001
 OPTIONS-000003
 ext
 ext1
-marker.format-version.000005.018
+marker.format-version.000006.019
 marker.manifest.000001.MANIFEST-000001
 
 open
@@ -651,9 +651,9 @@ allowFlush
 get with-fs-logging
 small-00001
 ----
-read-at(194, 27): 000004.sst
-read-at(120, 74): 000004.sst
-read-at(0, 120): 000004.sst
+read-at(158, 41): 000004.sst
+read-at(199, 74): 000004.sst
+read-at(0, 158): 000004.sst
 small-00001:val-00001
 
 # When the key doesn't pass the bloom filter, we should see only two block
@@ -661,8 +661,8 @@ small-00001:val-00001
 get with-fs-logging
 small-00001-does-not-exist
 ----
-read-at(194, 27): 000004.sst
-read-at(120, 74): 000004.sst
+read-at(158, 41): 000004.sst
+read-at(199, 74): 000004.sst
 small-00001-does-not-exist: pebble: not found
 
 # When looking inside the large table, we will not read the bloom filter which
@@ -671,16 +671,16 @@ small-00001-does-not-exist: pebble: not found
 get with-fs-logging
 large-00001
 ----
-read-at(775373, 117): 000005.sst
-read-at(765608, 2268): 000005.sst
-read-at(0, 1114): 000005.sst
+read-at(1161265, 69): 000005.sst
+read-at(1155684, 2966): 000005.sst
+read-at(0, 2818): 000005.sst
 large-00001:val-00001
 
 # Same number of block reads for a key that doesn't exist.
 get with-fs-logging
 large-00001-does-not-exist
 ----
-read-at(775373, 117): 000005.sst
-read-at(765608, 2268): 000005.sst
-read-at(0, 1114): 000005.sst
+read-at(1161265, 69): 000005.sst
+read-at(1155684, 2966): 000005.sst
+read-at(0, 2818): 000005.sst
 large-00001-does-not-exist: pebble: not found
diff --git a/testdata/ingest b/testdata/ingest
index e798b782..49fadcac 100644
--- a/testdata/ingest
+++ b/testdata/ingest
@@ -53,7 +53,7 @@ Backing tables: 0 (0B)
 Virtual tables: 0 (0B)
 Local tables size: 569B
 Compression types: snappy: 1
-Block cache: 4 entries (88B)  hit rate: 23.1%
+Block cache: 3 entries (84B)  hit rate: 18.2%
 Table cache: 1 entries (784B)  hit rate: 50.0%
 Secondary cache: 0 entries (0B)  hit rate: 0.0%
 Snapshots: 0  earliest seq num: 0
@@ -359,7 +359,7 @@ num-entries: 2
 num-deletions: 2
 num-range-key-sets: 0
 point-deletions-bytes-estimate: 0
-range-deletions-bytes-estimate: 1166
+range-deletions-bytes-estimate: 1226
 
 # A set operation takes precedence over a range deletion at the same
 # sequence number as can occur during ingestion.
diff --git a/testdata/ingest_external b/testdata/ingest_external
index 04e8942b..acb60cab 100644
--- a/testdata/ingest_external
+++ b/testdata/ingest_external
@@ -330,8 +330,8 @@ gi: (foo, .)
 lsm verbose
 ----
 L6:
-  000004(000004):[gc#10,DELSIZED-gf#inf,RANGEDEL] seqnums:[10-10] points:[gc#10,DELSIZED-gf#inf,RANGEDEL] size:947
-  000005(000005):[gg#11,DELSIZED-gj#inf,RANGEDEL] seqnums:[11-11] points:[gg#11,DELSIZED-gj#inf,RANGEDEL] size:628
+  000004(000004):[gc#10,DELSIZED-gf#inf,RANGEDEL] seqnums:[10-10] points:[gc#10,DELSIZED-gf#inf,RANGEDEL] size:1254
+  000005(000005):[gg#11,DELSIZED-gj#inf,RANGEDEL] seqnums:[11-11] points:[gg#11,DELSIZED-gj#inf,RANGEDEL] size:968
 
 download g h via-backing-file-download
 ----
@@ -341,8 +341,8 @@ ok
 lsm verbose
 ----
 L6:
-  000006(000006):[gc#10,DELSIZED-gf#inf,RANGEDEL] seqnums:[10-10] points:[gc#10,DELSIZED-gf#inf,RANGEDEL] size:712
-  000007(000007):[gg#11,DELSIZED-gj#inf,RANGEDEL] seqnums:[11-11] points:[gg#11,DELSIZED-gj#inf,RANGEDEL] size:612
+  000006(000006):[gc#10,DELSIZED-gf#inf,RANGEDEL] seqnums:[10-10] points:[gc#10,DELSIZED-gf#inf,RANGEDEL] size:874
+  000007(000007):[gg#11,DELSIZED-gj#inf,RANGEDEL] seqnums:[11-11] points:[gg#11,DELSIZED-gj#inf,RANGEDEL] size:867
 
 reopen
 ----
diff --git a/testdata/iterator_stats b/testdata/iterator_stats
index 7c7b9347..087d445e 100644
--- a/testdata/iterator_stats
+++ b/testdata/iterator_stats
@@ -17,7 +17,7 @@ stats
 a: (1, .)
 c: (2, .)
 .
-stats: seeked 1 times (1 internal); stepped 2 times (2 internal); blocks: 57B cached; points: 2 (2B keys, 2B values)
+stats: seeked 1 times (1 internal); stepped 2 times (2 internal); blocks: 113B cached; points: 2 (2B keys, 2B values)
 
 # Perform the same operation again with a new iterator. It should yield
 # identical statistics.
@@ -31,7 +31,7 @@ stats
 a: (1, .)
 c: (2, .)
 .
-stats: seeked 1 times (1 internal); stepped 2 times (2 internal); blocks: 57B cached; points: 2 (2B keys, 2B values)
+stats: seeked 1 times (1 internal); stepped 2 times (2 internal); blocks: 113B cached; points: 2 (2B keys, 2B values)
 
 build ext2
 set d@10 d10
@@ -61,13 +61,13 @@ next
 stats
 ----
 c: (2, .)
-stats: seeked 1 times (1 internal); stepped 0 times (0 internal); blocks: 57B cached; points: 1 (1B keys, 1B values)
+stats: seeked 1 times (1 internal); stepped 0 times (0 internal); blocks: 113B cached; points: 1 (1B keys, 1B values)
 d@10: (d10, .)
 d@9: (d9, .)
-stats: seeked 1 times (1 internal); stepped 2 times (2 internal); blocks: 147B cached, 10B not cached (read time: 0s); points: 3 (8B keys, 6B values); separated: 1 (2B, 2B fetched)
+stats: seeked 1 times (1 internal); stepped 2 times (2 internal); blocks: 282B cached, 10B not cached (read time: 0s); points: 3 (8B keys, 6B values); separated: 1 (2B, 2B fetched)
 d@8: (d8, .)
-stats: seeked 1 times (1 internal); stepped 3 times (3 internal); blocks: 147B cached, 10B not cached (read time: 0s); points: 4 (11B keys, 8B values); separated: 2 (4B, 4B fetched)
+stats: seeked 1 times (1 internal); stepped 3 times (3 internal); blocks: 282B cached, 10B not cached (read time: 0s); points: 4 (11B keys, 8B values); separated: 2 (4B, 4B fetched)
 e@20: (e20, .)
-stats: seeked 1 times (1 internal); stepped 4 times (4 internal); blocks: 147B cached, 10B not cached (read time: 0s); points: 5 (15B keys, 11B values); separated: 2 (4B, 4B fetched)
+stats: seeked 1 times (1 internal); stepped 4 times (4 internal); blocks: 282B cached, 10B not cached (read time: 0s); points: 5 (15B keys, 11B values); separated: 2 (4B, 4B fetched)
 e@18: (e18, .)
-stats: seeked 1 times (1 internal); stepped 5 times (5 internal); blocks: 147B cached, 10B not cached (read time: 0s); points: 6 (19B keys, 13B values); separated: 3 (7B, 7B fetched)
+stats: seeked 1 times (1 internal); stepped 5 times (5 internal); blocks: 282B cached, 10B not cached (read time: 0s); points: 6 (19B keys, 13B values); separated: 3 (7B, 7B fetched)
diff --git a/testdata/level_checker b/testdata/level_checker
index 45b3a8ab..2e1d004d 100644
--- a/testdata/level_checker
+++ b/testdata/level_checker
@@ -227,7 +227,7 @@ out of order keys b#2,SET >= b#3,SET in L1: fileNum=000023
 define write-unfragmented disable-key-order-checks
 L
 a.RANGEDEL.1 g.RANGEDEL.inf
-EncodeSpan:d-e:{(#2,RANGEDEL)} EncodeSpan:d-e:{(#1,RANGEDEL)} EncodeSpan:f-g:{(#3,RANGEDEL)} EncodeSpan:a-b:{(#4,RANGEDEL)}
+EncodeSpan:d-e:{(#2,RANGEDEL)-(#1,RANGEDEL)} EncodeSpan:f-g:{(#3,RANGEDEL)} EncodeSpan:a-b:{(#4,RANGEDEL)}
 ----
 Level 1
   file 0: [a#1,RANGEDEL-g#inf,RANGEDEL]
@@ -253,7 +253,7 @@ unordered or unfragmented range delete tombstones a-d:{(#1,RANGEDEL)}, b-c:{(#2,
 define write-unfragmented disable-key-order-checks
 L
 a.RANGEDEL.1 b.RANGEDEL.inf
-a.RANGEDEL.1:z d.RANGEDEL.2:e
+EncodeSpan:a-z:{(#1,RANGEDEL)} EncodeSpan:d-e:{(#2,RANGEDEL)}
 ----
 Level 1
   file 0: [a#1,RANGEDEL-b#inf,RANGEDEL]
diff --git a/testdata/manual_compaction_set_with_del_sstable_Pebblev5 b/testdata/manual_compaction_set_with_del_sstable_Pebblev5
new file mode 100644
index 00000000..0860e13b
--- /dev/null
+++ b/testdata/manual_compaction_set_with_del_sstable_Pebblev5
@@ -0,0 +1,818 @@
+batch
+set a 1
+set b 2
+----
+
+compact a-b
+----
+L6:
+  000005:[a#10,SET-b#11,SET]
+
+batch
+set c 3
+set d 4
+----
+
+compact c-d
+----
+L6:
+  000005:[a#10,SET-b#11,SET]
+  000007:[c#12,SET-d#13,SET]
+
+batch
+set b 5
+set c 6
+----
+
+compact a-d
+----
+L6:
+  000010:[a#0,SET-d#0,SET]
+
+# This also tests flushing a memtable that only contains range
+# deletions.
+
+batch
+del-range a e
+----
+
+compact a-d
+----
+
+# Test that a multi-output-file compaction generates non-overlapping files.
+
+define target-file-sizes=(100, 1)
+L0
+  b.SET.1:v
+L0
+  a.SET.2:v
+----
+L0.0:
+  000005:[a#2,SET-a#2,SET]
+  000004:[b#1,SET-b#1,SET]
+
+compact a-b
+----
+L1:
+  000006:[a#0,SET-a#0,SET]
+  000007:[b#0,SET-b#0,SET]
+
+# A range tombstone extends past the grandparent file boundary used to limit the
+# size of future compactions. Verify the range tombstone is split at that file
+# boundary.
+
+define target-file-sizes=(1, 1, 1, 1)
+L1
+  a.SET.3:v
+L2
+  a.RANGEDEL.2:e
+L3
+  a.SET.0:v
+  b.SET.0:v
+L3
+  c.SET.0:v
+  d.SET.0:v
+----
+L1:
+  000004:[a#3,SET-a#3,SET]
+L2:
+  000005:[a#2,RANGEDEL-e#inf,RANGEDEL]
+L3:
+  000006:[a#0,SET-b#0,SET]
+  000007:[c#0,SET-d#0,SET]
+
+wait-pending-table-stats
+000005
+----
+num-entries: 1
+num-deletions: 1
+num-range-key-sets: 0
+point-deletions-bytes-estimate: 0
+range-deletions-bytes-estimate: 1314
+
+compact a-e L1
+----
+L2:
+  000008:[a#3,SET-c#inf,RANGEDEL]
+  000009:[c#2,RANGEDEL-e#inf,RANGEDEL]
+L3:
+  000006:[a#0,SET-b#0,SET]
+  000007:[c#0,SET-d#0,SET]
+
+wait-pending-table-stats
+000008
+----
+num-entries: 2
+num-deletions: 1
+num-range-key-sets: 0
+point-deletions-bytes-estimate: 0
+range-deletions-bytes-estimate: 657
+
+# Same as above, except range tombstone covers multiple grandparent file boundaries.
+
+define target-file-sizes=(1, 1, 1, 1)
+L1
+  a.SET.3:v
+L2
+  a.RANGEDEL.2:g
+L3
+  a.SET.0:v
+  b.SET.0:v
+L3
+  c.SET.0:v
+  d.SET.0:v
+L3
+  e.SET.0:v
+  f.SET.1:v
+L3
+  g.SET.1:v
+  g.SET.0:v
+----
+L1:
+  000004:[a#3,SET-a#3,SET]
+L2:
+  000005:[a#2,RANGEDEL-g#inf,RANGEDEL]
+L3:
+  000006:[a#0,SET-b#0,SET]
+  000007:[c#0,SET-d#0,SET]
+  000008:[e#0,SET-f#1,SET]
+  000009:[g#1,SET-g#1,SET]
+
+compact a-e L1
+----
+L2:
+  000010:[a#3,SET-c#inf,RANGEDEL]
+  000011:[c#2,RANGEDEL-e#inf,RANGEDEL]
+  000012:[e#2,RANGEDEL-g#inf,RANGEDEL]
+L3:
+  000006:[a#0,SET-b#0,SET]
+  000007:[c#0,SET-d#0,SET]
+  000008:[e#0,SET-f#1,SET]
+  000009:[g#1,SET-g#1,SET]
+
+# A range tombstone covers multiple grandparent file boundaries between point keys,
+# rather than after all point keys.
+
+define target-file-sizes=(1, 1, 1, 1)
+L1
+  a.SET.3:v
+  h.SET.3:v
+L2
+  a.RANGEDEL.2:g
+L3
+  a.SET.0:v
+  b.SET.0:v
+L3
+  c.SET.0:v
+  d.SET.0:v
+L3
+  e.SET.0:v
+  f.SET.1:v
+----
+L1:
+  000004:[a#3,SET-h#3,SET]
+L2:
+  000005:[a#2,RANGEDEL-g#inf,RANGEDEL]
+L3:
+  000006:[a#0,SET-b#0,SET]
+  000007:[c#0,SET-d#0,SET]
+  000008:[e#0,SET-f#1,SET]
+
+compact a-e L1
+----
+L2:
+  000009:[a#3,SET-c#inf,RANGEDEL]
+  000010:[c#2,RANGEDEL-e#inf,RANGEDEL]
+  000011:[e#2,RANGEDEL-g#inf,RANGEDEL]
+  000012:[h#3,SET-h#3,SET]
+L3:
+  000006:[a#0,SET-b#0,SET]
+  000007:[c#0,SET-d#0,SET]
+  000008:[e#0,SET-f#1,SET]
+
+# A range tombstone is the first and only item output by a compaction, and it
+# extends past the grandparent file boundary used to limit the size of future
+# compactions. Verify the range tombstone is split at that file boundary.
+
+define target-file-sizes=(1, 1, 1, 1)
+L1
+  a.RANGEDEL.3:e
+L2
+  a.SET.2:v
+L3
+  a.SET.0:v
+  b.SET.0:v
+L3
+  c.SET.0:v
+  d.SET.0:v
+----
+L1:
+  000004:[a#3,RANGEDEL-e#inf,RANGEDEL]
+L2:
+  000005:[a#2,SET-a#2,SET]
+L3:
+  000006:[a#0,SET-b#0,SET]
+  000007:[c#0,SET-d#0,SET]
+
+compact a-e L1
+----
+L2:
+  000008:[a#3,RANGEDEL-c#inf,RANGEDEL]
+  000009:[c#3,RANGEDEL-e#inf,RANGEDEL]
+L3:
+  000006:[a#0,SET-b#0,SET]
+  000007:[c#0,SET-d#0,SET]
+
+# An elided range tombstone is the first item encountered by a compaction,
+# and the grandparent limit set by it extends to the next item, also a range
+# tombstone. The first item should be elided, and the second item should
+# reset the grandparent limit.
+
+define target-file-sizes=(100, 100, 100, 100)
+L1
+  a.RANGEDEL.4:d
+L1
+  grandparent.RANGEDEL.2:z
+  h.SET.3:v
+L2
+  grandparent.SET.1:v
+L3
+  grandparent.SET.0:v
+L3
+  m.SET.0:v
+----
+L1:
+  000004:[a#4,RANGEDEL-d#inf,RANGEDEL]
+  000005:[grandparent#2,RANGEDEL-z#inf,RANGEDEL]
+L2:
+  000006:[grandparent#1,SET-grandparent#1,SET]
+L3:
+  000007:[grandparent#0,SET-grandparent#0,SET]
+  000008:[m#0,SET-m#0,SET]
+
+compact a-h L1
+----
+L2:
+  000009:[grandparent#2,RANGEDEL-m#inf,RANGEDEL]
+  000010:[m#2,RANGEDEL-z#inf,RANGEDEL]
+L3:
+  000007:[grandparent#0,SET-grandparent#0,SET]
+  000008:[m#0,SET-m#0,SET]
+
+# Regression test for a bug where compaction would stop process range
+# tombstones for an input level upon finding an sstable in the input
+# level with no range tombstones. In the scenario below, sstable 6
+# does not contain any range tombstones while sstable 7 does. Both are
+# compacted together with sstable 5.
+
+reset
+----
+
+batch
+set a 1
+set b 1
+set c 1
+set d 1
+set z 1
+----
+
+compact a-z
+----
+L6:
+  000005:[a#10,SET-z#14,SET]
+
+build ext1
+set a 2
+----
+
+build ext2
+set b 2
+del-range c z
+----
+
+ingest ext1 ext2
+----
+L0.0:
+  000006:[a#15,SET-a#15,SET]
+  000007:[b#16,SET-z#inf,RANGEDEL]
+L6:
+  000005:[a#10,SET-z#14,SET]
+
+iter
+first
+next
+next
+next
+----
+a: (2, .)
+b: (2, .)
+z: (1, .)
+.
+
+compact a-z
+----
+L6:
+  000008:[a#0,SET-z#0,SET]
+
+iter
+first
+next
+next
+next
+----
+a: (2, .)
+b: (2, .)
+z: (1, .)
+.
+
+# Regression test for a bug in sstable smallest boundary generation
+# where the smallest key for an sstable was set to a key "larger" than
+# the start key of the first range tombstone. This in turn fouled up
+# the processing logic of range tombstones used by mergingIter which
+# allowed stepping out of an sstable even though it contained a range
+# tombstone that covered keys in lower levels.
+
+define target-file-sizes=(1, 1, 1, 1)
+L0
+  c.SET.4:4
+L1
+  a.SET.3:3
+L2
+  a.RANGEDEL.2:e
+L3
+  b.SET.1:1
+----
+L0.0:
+  000004:[c#4,SET-c#4,SET]
+L1:
+  000005:[a#3,SET-a#3,SET]
+L2:
+  000006:[a#2,RANGEDEL-e#inf,RANGEDEL]
+L3:
+  000007:[b#1,SET-b#1,SET]
+
+compact a-e L1
+----
+L0.0:
+  000004:[c#4,SET-c#4,SET]
+L2:
+  000008:[a#3,SET-b#inf,RANGEDEL]
+  000009:[b#2,RANGEDEL-e#inf,RANGEDEL]
+L3:
+  000007:[b#1,SET-b#1,SET]
+
+# We should only see a:3 and c:4 at this point.
+
+iter
+first
+next
+next
+----
+a: (3, .)
+c: (4, .)
+.
+
+# The bug allowed seeing b:1 during reverse iteration.
+
+iter
+last
+prev
+prev
+----
+c: (4, .)
+a: (3, .)
+.
+
+# This is a similar scenario to the one above. In older versions of Pebble this
+# case necessitated adjusting the seqnum of the range tombstone to
+# prev.LargestKey.SeqNum-1. We no longer allow user keys to be split across
+# sstables, and the seqnum adjustment is no longer necessary.
+#
+# Note the target-file-size of 26 is specially tailored to get the
+# desired compaction output.
+
+define target-file-sizes=(26, 26, 26, 26) snapshots=(1, 2, 3)
+L1
+  a.SET.4:4
+L1
+  b.SET.2:2
+  b.RANGEDEL.3:e
+L3
+  b.SET.1:1
+----
+L1:
+  000004:[a#4,SET-a#4,SET]
+  000005:[b#3,RANGEDEL-e#inf,RANGEDEL]
+L3:
+  000006:[b#1,SET-b#1,SET]
+
+compact a-e L1
+----
+L2:
+  000007:[a#4,SET-a#4,SET]
+  000008:[b#3,RANGEDEL-e#inf,RANGEDEL]
+L3:
+  000006:[b#1,SET-b#1,SET]
+
+iter
+first
+next
+last
+prev
+----
+a: (4, .)
+.
+a: (4, .)
+.
+
+# Similar to the preceding scenario, except the range tombstone has
+# the same seqnum as the largest key in the preceding file.
+
+define target-file-sizes=(26, 26, 26, 26) snapshots=(1, 2, 3)
+L1
+  a.SET.4:4
+L1
+  b.SET.3:3
+  b.RANGEDEL.3:e
+L3
+  b.SET.1:1
+----
+L1:
+  000004:[a#4,SET-a#4,SET]
+  000005:[b#3,RANGEDEL-e#inf,RANGEDEL]
+L3:
+  000006:[b#1,SET-b#1,SET]
+
+compact a-e L1
+----
+L2:
+  000007:[a#4,SET-a#4,SET]
+  000008:[b#3,RANGEDEL-e#inf,RANGEDEL]
+L3:
+  000006:[b#1,SET-b#1,SET]
+
+iter
+first
+next
+next
+last
+prev
+prev
+----
+a: (4, .)
+b: (3, .)
+.
+b: (3, .)
+a: (4, .)
+.
+
+# Similar to the preceding scenario, except the range tombstone has
+# a smaller seqnum than the largest key in the preceding file.
+
+define target-file-sizes=(26, 26, 26, 26) snapshots=(1, 2, 3)
+L1
+  a.SET.4:4
+L1
+  b.SET.4:4
+  b.RANGEDEL.2:e
+L3
+  b.SET.1:1
+----
+L1:
+  000004:[a#4,SET-a#4,SET]
+  000005:[b#4,SET-e#inf,RANGEDEL]
+L3:
+  000006:[b#1,SET-b#1,SET]
+
+compact a-e L1
+----
+L2:
+  000007:[a#4,SET-a#4,SET]
+  000008:[b#4,SET-e#inf,RANGEDEL]
+L3:
+  000006:[b#1,SET-b#1,SET]
+
+iter
+first
+next
+next
+last
+prev
+prev
+----
+a: (4, .)
+b: (4, .)
+.
+b: (4, .)
+a: (4, .)
+.
+
+# Test a scenario where the last point key in an sstable has a seqnum
+# of 0.
+
+define target-file-sizes=(1, 1, 26) snapshots=(2)
+L1
+  a.SET.3:3
+  b.RANGEDEL.3:e
+  b.SET.0:0
+L3
+  a.RANGEDEL.2:b
+L3
+  c.SET.0:0
+  d.SET.0:0
+----
+L1:
+  000004:[a#3,SET-e#inf,RANGEDEL]
+L3:
+  000005:[a#2,RANGEDEL-b#inf,RANGEDEL]
+  000006:[c#0,SET-d#0,SET]
+
+iter
+last
+prev
+----
+a: (3, .)
+.
+
+compact a-e L1
+----
+L2:
+  000007:[a#3,SET-a#3,SET]
+  000008:[b#3,RANGEDEL-c#inf,RANGEDEL]
+  000009:[c#3,RANGEDEL-e#inf,RANGEDEL]
+L3:
+  000005:[a#2,RANGEDEL-b#inf,RANGEDEL]
+  000006:[c#0,SET-d#0,SET]
+
+iter
+last
+prev
+----
+a: (3, .)
+.
+
+# Test a scenario where the last point key in an sstable before the
+# grandparent limit is reached has a seqnum of 0. We want to cut the
+# sstable after the next point key is added, rather than continuing to
+# add keys indefinitely (or till the size limit is reached).
+
+define target-file-sizes=(100, 1, 52) snapshots=(2)
+L1
+  a.SET.3:3
+  b.RANGEDEL.3:e
+  b.SET.0:0
+  c.SET.3:1
+  d.SET.1:1
+L3
+  c.RANGEDEL.2:d
+----
+L1:
+  000004:[a#3,SET-e#inf,RANGEDEL]
+L3:
+  000005:[c#2,RANGEDEL-d#inf,RANGEDEL]
+
+compact a-f L1
+----
+L2:
+  000006:[a#3,SET-a#3,SET]
+  000007:[b#3,RANGEDEL-c#inf,RANGEDEL]
+  000008:[c#3,RANGEDEL-d#inf,RANGEDEL]
+  000009:[d#3,RANGEDEL-e#inf,RANGEDEL]
+L3:
+  000005:[c#2,RANGEDEL-d#inf,RANGEDEL]
+
+
+# Test a scenario where we the last point key in an sstable has a
+# seqnum of 0, but there is another range tombstone later in the
+# compaction. This scenario was previously triggering an assertion due
+# to the rangedel.Fragmenter being finished prematurely.
+
+define target-file-sizes=(1, 1, 1)
+L1
+  a.SET.0:0
+  c.RANGEDEL.1:d
+L3
+  b.SET.0:0
+----
+L1:
+  000004:[a#0,SET-d#inf,RANGEDEL]
+L3:
+  000005:[b#0,SET-b#0,SET]
+
+compact a-e L1
+----
+L2:
+  000006:[a#0,SET-a#0,SET]
+L3:
+  000005:[b#0,SET-b#0,SET]
+
+define target-file-sizes=(1, 1, 1, 1)
+L0
+  b.SET.1:v
+L0
+  a.SET.2:v
+----
+L0.0:
+  000005:[a#2,SET-a#2,SET]
+  000004:[b#1,SET-b#1,SET]
+
+add-ongoing-compaction startLevel=0 outputLevel=1 start=a end=z
+----
+
+async-compact a-b L0
+----
+manual compaction blocked until ongoing finished
+L1:
+  000006:[a#0,SET-a#0,SET]
+  000007:[b#0,SET-b#0,SET]
+
+compact a-b L1
+----
+L2:
+  000008:[a#0,SET-a#0,SET]
+  000009:[b#0,SET-b#0,SET]
+
+add-ongoing-compaction startLevel=0 outputLevel=1 start=a end=z
+----
+
+async-compact a-b L2
+----
+manual compaction blocked until ongoing finished
+L3:
+  000010:[a#0,SET-a#0,SET]
+  000011:[b#0,SET-b#0,SET]
+
+add-ongoing-compaction startLevel=0 outputLevel=1 start=a end=z
+----
+
+set-concurrent-compactions num=2
+----
+
+async-compact a-b L3
+----
+manual compaction did not block for ongoing
+L4:
+  000012:[a#0,SET-a#0,SET]
+  000013:[b#0,SET-b#0,SET]
+
+remove-ongoing-compaction
+----
+
+add-ongoing-compaction startLevel=4 outputLevel=5 start=a end=b
+----
+
+async-compact a-b L4
+----
+manual compaction blocked until ongoing finished
+L5:
+  000014:[a#0,SET-a#0,SET]
+  000015:[b#0,SET-b#0,SET]
+
+# Test of a scenario where consecutive elided range tombstones and grandparent
+# boundaries could result in an invariant violation in the rangedel fragmenter.
+
+define target-file-sizes=(1, 1, 1, 1)
+L1
+  a.RANGEDEL.4:b
+  c.RANGEDEL.4:d
+  e.RANGEDEL.4:f
+L1
+  g.RANGEDEL.6:h
+  i.RANGEDEL.4:j
+L1
+  k.RANGEDEL.5:q
+  m.RANGEDEL.4:q
+L2
+  a.SET.2:foo
+L3
+  a.SET.1:foo
+  c.SET.1:foo
+L3
+  ff.SET.1:v
+L3
+  k.SET.1:foo
+----
+L1:
+  000004:[a#4,RANGEDEL-f#inf,RANGEDEL]
+  000005:[g#6,RANGEDEL-j#inf,RANGEDEL]
+  000006:[k#5,RANGEDEL-q#inf,RANGEDEL]
+L2:
+  000007:[a#2,SET-a#2,SET]
+L3:
+  000008:[a#1,SET-c#1,SET]
+  000009:[ff#1,SET-ff#1,SET]
+  000010:[k#1,SET-k#1,SET]
+
+compact a-q L1
+----
+L2:
+  000011:[a#4,RANGEDEL-b#inf,RANGEDEL]
+  000012:[c#4,RANGEDEL-d#inf,RANGEDEL]
+  000013:[k#5,RANGEDEL-m#inf,RANGEDEL]
+L3:
+  000008:[a#1,SET-c#1,SET]
+  000009:[ff#1,SET-ff#1,SET]
+  000010:[k#1,SET-k#1,SET]
+
+# Test a case where a new output file is started, there are no previous output
+# files, there are no additional keys (key = nil) and the rangedel fragmenter
+# is non-empty.
+define target-file-sizes=(1, 1, 1)
+L1
+  a.RANGEDEL.10:b
+  d.RANGEDEL.9:e
+  q.RANGEDEL.8:r
+L2
+  g.RANGEDEL.7:h
+L3
+  q.SET.6:6
+----
+L1:
+  000004:[a#10,RANGEDEL-r#inf,RANGEDEL]
+L2:
+  000005:[g#7,RANGEDEL-h#inf,RANGEDEL]
+L3:
+  000006:[q#6,SET-q#6,SET]
+
+compact a-r L1
+----
+L2:
+  000007:[q#8,RANGEDEL-r#inf,RANGEDEL]
+L3:
+  000006:[q#6,SET-q#6,SET]
+
+# Test a snapshot that separates a range deletion from all the data that it
+# deletes. Ensure that we respect the target-file-size and split into multiple
+# outputs.
+
+define target-file-sizes=(1, 1, 1) snapshots=(14)
+L1
+  a.RANGEDEL.15:z
+  b.SET.11:foo
+  c.SET.11:foo
+L2
+  c.SET.0:foo
+  d.SET.0:foo
+----
+L1:
+  000004:[a#15,RANGEDEL-z#inf,RANGEDEL]
+L2:
+  000005:[c#0,SET-d#0,SET]
+
+compact a-z L1
+----
+L2:
+  000006:[a#15,RANGEDEL-b#inf,RANGEDEL]
+  000007:[b#15,RANGEDEL-c#inf,RANGEDEL]
+  000008:[c#15,RANGEDEL-d#inf,RANGEDEL]
+  000009:[d#15,RANGEDEL-z#inf,RANGEDEL]
+
+# Test an interaction between a range deletion that will be elided with
+# output splitting. Ensure that the output is still split (previous versions
+# of the code did not, because of intricacies around preventing a zero
+# sequence number in an output's largest key).
+
+define target-file-sizes=(1, 1, 1)
+L1
+  a.RANGEDEL.10:z
+  b.SET.11:foo
+  c.SET.11:foo
+L2
+  c.SET.0:foo
+  d.SET.0:foo
+----
+L1:
+  000004:[a#10,RANGEDEL-z#inf,RANGEDEL]
+L2:
+  000005:[c#0,SET-d#0,SET]
+
+compact a-z L1
+----
+L2:
+  000006:[b#0,SET-b#0,SET]
+  000007:[c#0,SET-c#0,SET]
+
+define snapshots=(10)
+L1
+  a.MERGE.15:a15
+L2
+  a.SET.5:a5
+----
+L1:
+  000004:[a#15,MERGE-a#15,MERGE]
+L2:
+  000005:[a#5,SET-a#5,SET]
+
+compact a-z
+----
+L3:
+  000006:[a#15,MERGE-a#0,SET]
+
+# Fix for #2705. When snapshotPinned was used to set force obsolete, the
+# merged value would be a15 since the SET was incorrectly ignored.
+iter
+first
+next
+----
+a: (a5a15, .)
+.
diff --git a/testdata/marked_for_compaction b/testdata/marked_for_compaction
index 98f343e3..3d1b34c9 100644
--- a/testdata/marked_for_compaction
+++ b/testdata/marked_for_compaction
@@ -6,9 +6,9 @@ L1
   d.SET.0:foo
 ----
 L0.0:
-  000004:[c#11,SET-c#11,SET] seqnums:[11-11] points:[c#11,SET-c#11,SET] size:593
+  000004:[c#11,SET-c#11,SET] seqnums:[11-11] points:[c#11,SET-c#11,SET] size:653
 L1:
-  000005:[c#0,SET-d#0,SET] seqnums:[0-0] points:[c#0,SET-d#0,SET] size:598
+  000005:[c#0,SET-d#0,SET] seqnums:[0-0] points:[c#0,SET-d#0,SET] size:661
 
 mark-for-compaction file=000005
 ----
@@ -20,9 +20,9 @@ marked L0.000004
 
 maybe-compact
 ----
-[JOB 100] compacted(rewrite) L1 [000005] (598B) Score=0.00 + L1 [] (0B) Score=0.00 -> L1 [000006] (598B), in 1.0s (2.0s total), output rate 598B/s
-[JOB 100] compacted(rewrite) L0 [000004] (593B) Score=0.00 + L0 [] (0B) Score=0.00 -> L0 [000007] (593B), in 1.0s (2.0s total), output rate 593B/s
+[JOB 100] compacted(rewrite) L1 [000005] (661B) Score=0.00 + L1 [] (0B) Score=0.00 -> L1 [000006] (661B), in 1.0s (2.0s total), output rate 661B/s
+[JOB 100] compacted(rewrite) L0 [000004] (653B) Score=0.00 + L0 [] (0B) Score=0.00 -> L0 [000007] (653B), in 1.0s (2.0s total), output rate 653B/s
 L0.0:
-  000007:[c#11,SET-c#11,SET] seqnums:[11-11] points:[c#11,SET-c#11,SET] size:593
+  000007:[c#11,SET-c#11,SET] seqnums:[11-11] points:[c#11,SET-c#11,SET] size:653
 L1:
-  000006:[c#0,SET-d#0,SET] seqnums:[0-0] points:[c#0,SET-d#0,SET] size:598
+  000006:[c#0,SET-d#0,SET] seqnums:[0-0] points:[c#0,SET-d#0,SET] size:661
diff --git a/testdata/metrics b/testdata/metrics
index 9c74ca85..ab5d8ae2 100644
--- a/testdata/metrics
+++ b/testdata/metrics
@@ -476,32 +476,32 @@ metrics
       |                             |       |       |   ingested   |     moved    |    written   |       |    amp
 level | tables  size val-bl vtables | score |   in  | tables  size | tables  size | tables  size |  read |   r   w
 ------+-----------------------------+-------+-------+--------------+--------------+--------------+-------+---------
-    0 |     4  2.3KB     0B       0 |  0.50 |  187B |     3  1.7KB |     0     0B |     6  3.8KB |    0B |   2 20.6
+    0 |     4  2.5KB     0B       0 |  0.50 |  187B |     3  1.9KB |     0     0B |     6  3.8KB |    0B |   2 20.6
     1 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     2 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     3 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     4 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     5 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     6 |     3  2.0KB    41B       0 |     - | 3.2KB |     0     0B |     0     0B |     3  2.0KB | 3.2KB |   1  0.6
-total |     7  4.3KB    41B       0 |     - | 1.9KB |     3  1.7KB |     0     0B |     9  7.7KB | 3.2KB |   3  4.0
+total |     7  4.5KB    41B       0 |     - | 2.1KB |     3  1.9KB |     0     0B |     9  7.9KB | 3.2KB |   3  3.8
 -------------------------------------------------------------------------------------------------------------------
 WAL: 1 files (0B)  in: 176B  written: 187B (6% overhead)
 Flushes: 8
-Compactions: 2  estimated debt: 4.3KB  in progress: 0 (0B)
+Compactions: 2  estimated debt: 4.5KB  in progress: 0 (0B)
              default: 2  delete: 0  elision: 0  move: 0  read: 0  tombstone-density: 0  rewrite: 0  copy: 0  multi-level: 0
 MemTables: 1 (1.0MB)  zombie: 1 (1.0MB)
 Zombie tables: 0 (0B, local: 0B)
 Backing tables: 0 (0B)
 Virtual tables: 0 (0B)
-Local tables size: 4.3KB
+Local tables size: 4.5KB
 Compression types: snappy: 7
-Block cache: 8 entries (204B)  hit rate: 9.1%
+Block cache: 8 entries (441B)  hit rate: 9.1%
 Table cache: 1 entries (784B)  hit rate: 53.8%
 Secondary cache: 0 entries (0B)  hit rate: 0.0%
 Snapshots: 0  earliest seq num: 0
 Table iters: 0
 Filter utility: 0.0%
-Ingestions: 2  as flushable: 2 (1.7KB in 3 tables)
+Ingestions: 2  as flushable: 2 (1.9KB in 3 tables)
 Cgo memory usage: 0B  block cache: 0B (data: 0B, maps: 0B, entries: 0B)  memtables: 0B
 Iter category stats:
                    b,     latency: {BlockBytes:44 BlockBytesInCache:0 BlockReadDuration:10ms}
@@ -540,32 +540,32 @@ metrics
       |                             |       |       |   ingested   |     moved    |    written   |       |    amp
 level | tables  size val-bl vtables | score |   in  | tables  size | tables  size | tables  size |  read |   r   w
 ------+-----------------------------+-------+-------+--------------+--------------+--------------+-------+---------
-    0 |     7  4.1KB     0B       0 |  0.50 |  245B |     3  1.7KB |     0     0B |     9  5.5KB |    0B |   2 23.1
+    0 |     7  4.3KB     0B       0 |  0.50 |  245B |     3  1.9KB |     0     0B |     9  5.5KB |    0B |   2 23.1
     1 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     2 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     3 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     4 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     5 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     6 |     3  2.0KB    41B       0 |     - | 3.2KB |     0     0B |     0     0B |     3  2.0KB | 3.2KB |   1  0.6
-total |    10  6.1KB    41B       0 |     - | 2.0KB |     3  1.7KB |     0     0B |    12  9.5KB | 3.2KB |   3  4.8
+total |    10  6.3KB    41B       0 |     - | 2.2KB |     3  1.9KB |     0     0B |    12  9.7KB | 3.2KB |   3  4.5
 -------------------------------------------------------------------------------------------------------------------
 WAL: 1 files (0B)  in: 223B  written: 245B (10% overhead)
 Flushes: 9
-Compactions: 2  estimated debt: 6.1KB  in progress: 0 (0B)
+Compactions: 2  estimated debt: 6.3KB  in progress: 0 (0B)
              default: 2  delete: 0  elision: 0  move: 0  read: 0  tombstone-density: 0  rewrite: 0  copy: 0  multi-level: 0
 MemTables: 1 (1.0MB)  zombie: 1 (1.0MB)
 Zombie tables: 0 (0B, local: 0B)
 Backing tables: 0 (0B)
 Virtual tables: 0 (0B)
-Local tables size: 6.1KB
+Local tables size: 6.3KB
 Compression types: snappy: 10
-Block cache: 8 entries (204B)  hit rate: 9.1%
+Block cache: 8 entries (441B)  hit rate: 9.1%
 Table cache: 1 entries (784B)  hit rate: 53.8%
 Secondary cache: 0 entries (0B)  hit rate: 0.0%
 Snapshots: 0  earliest seq num: 0
 Table iters: 0
 Filter utility: 0.0%
-Ingestions: 2  as flushable: 2 (1.7KB in 3 tables)
+Ingestions: 2  as flushable: 2 (1.9KB in 3 tables)
 Cgo memory usage: 0B  block cache: 0B (data: 0B, maps: 0B, entries: 0B)  memtables: 0B
 Iter category stats:
                    b,     latency: {BlockBytes:44 BlockBytesInCache:0 BlockReadDuration:10ms}
@@ -618,24 +618,24 @@ metrics zero-cache-hits-misses
       |                             |       |       |   ingested   |     moved    |    written   |       |    amp
 level | tables  size val-bl vtables | score |   in  | tables  size | tables  size | tables  size |  read |   r   w
 ------+-----------------------------+-------+-------+--------------+--------------+--------------+-------+---------
-    0 |     7  3.0KB     0B       2 |  0.50 |  245B |     3  1.7KB |     0     0B |     9  5.5KB |    0B |   2 23.1
+    0 |     7  3.2KB     0B       2 |  0.50 |  245B |     3  1.9KB |     0     0B |     9  5.5KB |    0B |   2 23.1
     1 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     2 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     3 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     4 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     5 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
-    6 |     4  2.6KB    41B       0 |     - | 3.2KB |     1   589B |     0     0B |     3  2.0KB | 3.2KB |   1  0.6
-total |    11  5.6KB    41B       2 |     - | 2.5KB |     4  2.3KB |     0     0B |    12   10KB | 3.2KB |   3  4.0
+    6 |     4  2.7KB    41B       0 |     - | 3.2KB |     1   654B |     0     0B |     3  2.0KB | 3.2KB |   1  0.6
+total |    11  5.8KB    41B       2 |     - | 2.8KB |     4  2.6KB |     0     0B |    12   10KB | 3.2KB |   3  3.7
 -------------------------------------------------------------------------------------------------------------------
 WAL: 1 files (0B)  in: 223B  written: 245B (10% overhead)
 Flushes: 9
-Compactions: 2  estimated debt: 5.6KB  in progress: 0 (0B)
+Compactions: 2  estimated debt: 5.8KB  in progress: 0 (0B)
              default: 2  delete: 0  elision: 0  move: 0  read: 0  tombstone-density: 0  rewrite: 0  copy: 0  multi-level: 0
 MemTables: 1 (1.0MB)  zombie: 1 (1.0MB)
 Zombie tables: 0 (0B, local: 0B)
 Backing tables: 2 (1.2KB)
 Virtual tables: 2 (102B)
-Local tables size: 6.7KB
+Local tables size: 6.9KB
 Compression types: snappy: 9 unknown: 2
 Block cache: 0 entries (0B)  hit rate: 0.0%
 Table cache: 0 entries (0B)  hit rate: 0.0%
@@ -643,7 +643,7 @@ Secondary cache: 0 entries (0B)  hit rate: 0.0%
 Snapshots: 0  earliest seq num: 0
 Table iters: 0
 Filter utility: 0.0%
-Ingestions: 3  as flushable: 2 (1.7KB in 3 tables)
+Ingestions: 3  as flushable: 2 (1.9KB in 3 tables)
 Cgo memory usage: 0B  block cache: 0B (data: 0B, maps: 0B, entries: 0B)  memtables: 0B
 Iter category stats:
                    b,     latency: {BlockBytes:44 BlockBytesInCache:0 BlockReadDuration:10ms}
@@ -721,14 +721,14 @@ metrics zero-cache-hits-misses
       |                             |       |       |   ingested   |     moved    |    written   |       |    amp
 level | tables  size val-bl vtables | score |   in  | tables  size | tables  size | tables  size |  read |   r   w
 ------+-----------------------------+-------+-------+--------------+--------------+--------------+-------+---------
-    0 |     0     0B     0B       0 |  0.00 |  245B |     3  1.7KB |     0     0B |     9  5.5KB |    0B |   0 23.1
+    0 |     0     0B     0B       0 |  0.00 |  245B |     3  1.9KB |     0     0B |     9  5.5KB |    0B |   0 23.1
     1 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     2 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     3 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     4 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     5 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
-    6 |     6  3.8KB    41B       0 |     - | 6.2KB |     2  1.2KB |     0     0B |     4  2.6KB | 6.2KB |   1  0.4
-total |     6  3.8KB    41B       0 |     - | 3.1KB |     5  2.9KB |     0     0B |    13   11KB | 6.2KB |   1  3.6
+    6 |     6  3.9KB    41B       0 |     - | 6.4KB |     2  1.3KB |     0     0B |     4  2.6KB | 6.4KB |   1  0.4
+total |     6  3.9KB    41B       0 |     - | 3.4KB |     5  3.2KB |     0     0B |    13   12KB | 6.4KB |   1  3.4
 -------------------------------------------------------------------------------------------------------------------
 WAL: 1 files (0B)  in: 223B  written: 245B (10% overhead)
 Flushes: 9
@@ -738,7 +738,7 @@ MemTables: 1 (1.0MB)  zombie: 1 (1.0MB)
 Zombie tables: 0 (0B, local: 0B)
 Backing tables: 0 (0B)
 Virtual tables: 0 (0B)
-Local tables size: 3.8KB
+Local tables size: 3.9KB
 Compression types: snappy: 6
 Block cache: 0 entries (0B)  hit rate: 0.0%
 Table cache: 0 entries (0B)  hit rate: 0.0%
@@ -746,12 +746,12 @@ Secondary cache: 0 entries (0B)  hit rate: 0.0%
 Snapshots: 0  earliest seq num: 0
 Table iters: 0
 Filter utility: 0.0%
-Ingestions: 4  as flushable: 2 (1.7KB in 3 tables)
+Ingestions: 4  as flushable: 2 (1.9KB in 3 tables)
 Cgo memory usage: 0B  block cache: 0B (data: 0B, maps: 0B, entries: 0B)  memtables: 0B
 Iter category stats:
                    b,     latency: {BlockBytes:44 BlockBytesInCache:0 BlockReadDuration:10ms}
                    c, non-latency: {BlockBytes:44 BlockBytesInCache:44 BlockReadDuration:0s}
-   pebble-compaction, non-latency: {BlockBytes:677 BlockBytesInCache:376 BlockReadDuration:70ms}
+   pebble-compaction, non-latency: {BlockBytes:872 BlockBytesInCache:571 BlockReadDuration:70ms}
        pebble-ingest,     latency: {BlockBytes:272 BlockBytesInCache:72 BlockReadDuration:30ms}
 
 # Create a DB where lower levels are written as shared tables. All ingests also
@@ -863,14 +863,14 @@ metrics
       |                             |       |       |   ingested   |     moved    |    written   |       |    amp
 level | tables  size val-bl vtables | score |   in  | tables  size | tables  size | tables  size |  read |   r   w
 ------+-----------------------------+-------+-------+--------------+--------------+--------------+-------+---------
-    0 |     1   589B     0B       0 |  0.25 |   38B |     1   589B |     0     0B |     1   604B |    0B |   1 15.9
+    0 |     1   654B     0B       0 |  0.25 |   38B |     1   654B |     0     0B |     1   604B |    0B |   1 15.9
     1 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     2 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     3 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     4 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     5 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     6 |     1   604B     0B       0 |     - |  604B |     0     0B |     0     0B |     1   604B |    0B |   1  1.0
-total |     2  1.2KB     0B       0 |     - |  627B |     1   589B |     0     0B |     2  1.8KB |    0B |   2  2.9
+total |     2  1.2KB     0B       0 |     - |  692B |     1   654B |     0     0B |     2  1.9KB |    0B |   2  2.7
 -------------------------------------------------------------------------------------------------------------------
 WAL: 1 files (0B)  in: 27B  written: 38B (41% overhead)
 Flushes: 1
@@ -882,7 +882,7 @@ Backing tables: 0 (0B)
 Virtual tables: 0 (0B)
 Local tables size: 0B
 Compression types: snappy: 2
-Block cache: 4 entries (116B)  hit rate: 0.0%
+Block cache: 4 entries (195B)  hit rate: 0.0%
 Table cache: 1 entries (784B)  hit rate: 50.0%
 Secondary cache: 0 entries (0B)  hit rate: 0.0%
 Snapshots: 0  earliest seq num: 0
@@ -912,18 +912,18 @@ metrics
       |                             |       |       |   ingested   |     moved    |    written   |       |    amp
 level | tables  size val-bl vtables | score |   in  | tables  size | tables  size | tables  size |  read |   r   w
 ------+-----------------------------+-------+-------+--------------+--------------+--------------+-------+---------
-    0 |     2  1.2KB     0B       0 |  0.50 |   66B |     1   589B |     0     0B |     2  1.2KB |    0B |   2 18.1
+    0 |     2  1.2KB     0B       0 |  0.50 |   66B |     1   654B |     0     0B |     2  1.2KB |    0B |   2 18.1
     1 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     2 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     3 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     4 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     5 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     6 |     1   604B     0B       0 |     - |  604B |     0     0B |     0     0B |     1   604B |    0B |   1  1.0
-total |     3  1.7KB     0B       0 |     - |  655B |     1   589B |     0     0B |     3  2.4KB |    0B |   3  3.7
+total |     3  1.8KB     0B       0 |     - |  720B |     1   654B |     0     0B |     3  2.5KB |    0B |   3  3.5
 -------------------------------------------------------------------------------------------------------------------
 WAL: 1 files (0B)  in: 44B  written: 66B (50% overhead)
 Flushes: 2
-Compactions: 1  estimated debt: 1.7KB  in progress: 0 (0B)
+Compactions: 1  estimated debt: 1.8KB  in progress: 0 (0B)
              default: 0  delete: 0  elision: 0  move: 0  read: 0  tombstone-density: 0  rewrite: 0  copy: 1  multi-level: 0
 MemTables: 1 (256KB)  zombie: 1 (256KB)
 Zombie tables: 0 (0B, local: 0B)
@@ -931,7 +931,7 @@ Backing tables: 0 (0B)
 Virtual tables: 0 (0B)
 Local tables size: 589B
 Compression types: snappy: 3
-Block cache: 4 entries (116B)  hit rate: 0.0%
+Block cache: 4 entries (195B)  hit rate: 0.0%
 Table cache: 1 entries (784B)  hit rate: 50.0%
 Secondary cache: 0 entries (0B)  hit rate: 0.0%
 Snapshots: 0  earliest seq num: 0
@@ -959,11 +959,11 @@ level | tables  size val-bl vtables | score |   in  | tables  size | tables  siz
     4 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     5 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     6 |     1   604B     0B       0 |     - |    0B |     0     0B |     0     0B |     0     0B |    0B |   1  0.0
-total |     3  1.7KB     0B       0 |     - |    0B |     0     0B |     0     0B |     0     0B |    0B |   3  0.0
+total |     3  1.8KB     0B       0 |     - |    0B |     0     0B |     0     0B |     0     0B |    0B |   3  0.0
 -------------------------------------------------------------------------------------------------------------------
 WAL: 1 files (0B)  in: 0B  written: 0B (0% overhead)
 Flushes: 1
-Compactions: 0  estimated debt: 1.7KB  in progress: 0 (0B)
+Compactions: 0  estimated debt: 1.8KB  in progress: 0 (0B)
              default: 0  delete: 0  elision: 0  move: 0  read: 0  tombstone-density: 0  rewrite: 0  copy: 0  multi-level: 0
 MemTables: 1 (512KB)  zombie: 1 (256KB)
 Zombie tables: 0 (0B, local: 0B)
@@ -997,8 +997,8 @@ level | tables  size val-bl vtables | score |   in  | tables  size | tables  siz
     3 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     4 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
     5 |     0     0B     0B       0 |  0.00 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0  0.0
-    6 |     1   603B     0B       0 |     - | 1.2KB |     0     0B |     0     0B |     1   603B | 1.7KB |   1  0.5
-total |     1   603B     0B       0 |     - |    0B |     0     0B |     0     0B |     1   603B | 1.7KB |   1  0.0
+    6 |     1   603B     0B       0 |     - | 1.2KB |     0     0B |     0     0B |     1   603B | 1.8KB |   1  0.5
+total |     1   603B     0B       0 |     - |    0B |     0     0B |     0     0B |     1   603B | 1.8KB |   1  0.0
 -------------------------------------------------------------------------------------------------------------------
 WAL: 1 files (0B)  in: 0B  written: 0B (0% overhead)
 Flushes: 1
@@ -1019,4 +1019,4 @@ Filter utility: 0.0%
 Ingestions: 0  as flushable: 0 (0B in 0 tables)
 Cgo memory usage: 0B  block cache: 0B (data: 0B, maps: 0B, entries: 0B)  memtables: 0B
 Iter category stats:
-   pebble-compaction, non-latency: {BlockBytes:147 BlockBytesInCache:0 BlockReadDuration:30ms}
+   pebble-compaction, non-latency: {BlockBytes:212 BlockBytesInCache:0 BlockReadDuration:30ms}