Repository: cockroachdb/pebble
Pull request #4510 (2025-04-08 23:52:18)

*: add errcheck linter and fix all reported cases
We exclude `_test.go` files and a list of specific functions.

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/4510)
<!-- Reviewable:end -->


Patch:
diff --git a/batch.go b/batch.go
index 63625fb1..0d5d7a0e 100644
--- a/batch.go
+++ b/batch.go
@@ -1451,7 +1451,7 @@ func (b *Batch) initRangeKeyIter(_ *IterOptions, iter *keyspan.Iter, batchSnapsh
 		iter:     b.rangeKeyIndex.NewIter(nil, nil),
 		snapshot: batchSnapshot,
 	}
-	fragmentRangeKeys(frag, it, int(b.countRangeKeys))
+	_ = fragmentRangeKeys(frag, it, int(b.countRangeKeys))
 	iter.Init(b.comparer.Compare, rangeKeys)
 
 	// If we just read all the range keys in the batch (eg, batchSnapshot was
@@ -2026,7 +2026,9 @@ func newFlushableBatch(batch *Batch, comparer *Comparer) (*flushableBatch, error
 			cmp:     b.cmp,
 			index:   -1,
 		}
-		fragmentRangeKeys(frag, it, len(rangeKeyOffsets))
+		if err := fragmentRangeKeys(frag, it, len(rangeKeyOffsets)); err != nil {
+			return nil, err
+		}
 	}
 	return b, nil
 }
diff --git a/cmd/pebble/fsbench.go b/cmd/pebble/fsbench.go
index 39c46055..a2874bab 100644
--- a/cmd/pebble/fsbench.go
+++ b/cmd/pebble/fsbench.go
@@ -58,7 +58,7 @@ func init() {
 
 	fsBenchCmd.Flags().StringVar(
 		&fsConfig.benchname, "bench-name", "", "The benchmark to run.")
-	fsBenchCmd.MarkFlagRequired("bench-name")
+	_ = fsBenchCmd.MarkFlagRequired("bench-name")
 
 	fsBenchCmd.Flags().IntVar(
 		&fsConfig.numTimes, "num-times", 1,
diff --git a/cmd/pebble/mvcc.go b/cmd/pebble/mvcc.go
index 544946c9..74dae07d 100644
--- a/cmd/pebble/mvcc.go
+++ b/cmd/pebble/mvcc.go
@@ -20,7 +20,7 @@ func mvccForwardScan(d DB, start, end, ts []byte) (int, int64) {
 		LowerBound: cockroachkvs.EncodeMVCCKey(nil, start, 0, 0),
 		UpperBound: cockroachkvs.EncodeMVCCKey(nil, end, 0, 0),
 	})
-	defer it.Close()
+	defer func() { _ = it.Close() }()
 
 	var data bytealloc.A
 	var count int
@@ -44,7 +44,7 @@ func mvccReverseScan(d DB, start, end, ts []byte) (int, int64) {
 		LowerBound: cockroachkvs.EncodeMVCCKey(nil, start, 0, 0),
 		UpperBound: cockroachkvs.EncodeMVCCKey(nil, end, 0, 0),
 	})
-	defer it.Close()
+	defer func() { _ = it.Close() }()
 
 	var data bytealloc.A
 	var count int
diff --git a/cmd/pebble/queue.go b/cmd/pebble/queue.go
index 1e425304..fbd23510 100644
--- a/cmd/pebble/queue.go
+++ b/cmd/pebble/queue.go
@@ -50,7 +50,9 @@ func queueTest() (test, *atomic.Int64) {
 				b := d.NewBatch()
 				queue[i] = cockroachkvs.EncodeMVCCKey(nil, encodeUint32Ascending([]byte("queue-"), uint32(i)), uint64(i+1), 0)
 				value = queueConfig.values.Bytes(rng, value)
-				b.Set(queue[i], value, pebble.NoSync)
+				if err := b.Set(queue[i], value, pebble.NoSync); err != nil {
+					log.Fatal(err)
+				}
 				if err := b.Commit(pebble.NoSync); err != nil {
 					log.Fatal(err)
 				}
@@ -82,7 +84,9 @@ func queueTest() (test, *atomic.Int64) {
 					b = d.NewBatch()
 					queue[idx] = cockroachkvs.EncodeMVCCKey(queue[idx][:0], encodeUint32Ascending([]byte("queue-"), uint32(i)), uint64(i+1), 0)
 					value = queueConfig.values.Bytes(rng, value)
-					b.Set(queue[idx], value, nil)
+					if err := b.Set(queue[idx], value, nil); err != nil {
+						log.Fatal(err)
+					}
 					if err := b.Commit(pebble.Sync); err != nil {
 						log.Fatal(err)
 					}
diff --git a/cmd/pebble/replay.go b/cmd/pebble/replay.go
index 5b6b8bab..26edec7b 100644
--- a/cmd/pebble/replay.go
+++ b/cmd/pebble/replay.go
@@ -141,13 +141,13 @@ func (c *replayConfig) runE(cmd *cobra.Command, args []string) error {
 			return err
 		}
 		execArgs := append(append([]string{executable, "bench", "replay"}, c.args()...), workloadPath)
-		syscall.Exec(executable, execArgs, os.Environ())
+		_ = syscall.Exec(executable, execArgs, os.Environ())
 	}
 	return nil
 }
 
 func (c *replayConfig) runOnce(stdout io.Writer, workloadPath string) error {
-	defer c.cleanUp()
+	defer func() { _ = c.cleanUp() }()
 	if c.name == "" {
 		c.name = vfs.Default.PathBase(workloadPath)
 	}
diff --git a/cockroachkvs/cockroachkvs.go b/cockroachkvs/cockroachkvs.go
index ba253221..344ef2c1 100644
--- a/cockroachkvs/cockroachkvs.go
+++ b/cockroachkvs/cockroachkvs.go
@@ -1158,7 +1158,7 @@ func ParseFormattedKeySuffix(formattedSuffix string) []byte {
 		k := make([]byte, suffixLenWithLockTable)
 		// The lock strength is encoded as a hexadecimal digit (2 chars),
 		// followed by a comma.
-		hex.Decode(k, unsafe.Slice(unsafe.StringData(formattedSuffix), 2))
+		_, _ = hex.Decode(k, unsafe.Slice(unsafe.StringData(formattedSuffix), 2))
 		if err := decodeUUID(k[1:], unsafe.Slice(unsafe.StringData(formattedSuffix[3:]), len(formattedSuffix)-3)); err != nil {
 			panic(errors.Newf("invalid lock table suffix: %w", err))
 		}
diff --git a/commit.go b/commit.go
index cf2b665d..c86e378b 100644
--- a/commit.go
+++ b/commit.go
@@ -368,7 +368,7 @@ func (p *commitPipeline) AllocateSeqNum(
 	// performance critical code paths.
 
 	b := newBatch(nil)
-	defer b.Close()
+	defer func() { _ = b.Close() }()
 
 	// Give the batch a count of 1 so that the log and visible sequence number
 	// are incremented correctly.
diff --git a/compaction.go b/compaction.go
index bebe4723..f5129234 100644
--- a/compaction.go
+++ b/compaction.go
@@ -767,7 +767,7 @@ func (c *compaction) newInputIters(
 		if retErr != nil {
 			for _, iter := range iters {
 				if iter != nil {
-					iter.Close()
+					_ = iter.Close()
 				}
 			}
 			for _, rangeDelIter := range rangeDelIters {
@@ -1214,7 +1214,7 @@ func (d *DB) maybeScheduleDelayedFlush(tbl *memTable, dur time.Duration) {
 			}
 
 			if d.mu.mem.mutable == tbl {
-				d.makeRoomForWrite(nil)
+				_ = d.makeRoomForWrite(nil)
 			} else {
 				mem.flushForced = true
 			}
@@ -2615,7 +2615,7 @@ func (d *DB) runCopyCompaction(
 		}
 		defer func() {
 			if src != nil {
-				src.Close()
+				_ = src.Close()
 			}
 		}()
 
diff --git a/db.go b/db.go
index a86ade0d..a6355c15 100644
--- a/db.go
+++ b/db.go
@@ -2948,7 +2948,7 @@ func (d *DB) checkVirtualBounds(m *tableMetadata) {
 	if err != nil {
 		panic(errors.Wrap(err, "pebble: error creating iterators"))
 	}
-	defer iters.CloseAll()
+	defer func() { _ = iters.CloseAll() }()
 
 	if m.HasPointKeys {
 		pointIter := iters.Point()
diff --git a/excise.go b/excise.go
index 241ecb93..0ca3e569 100644
--- a/excise.go
+++ b/excise.go
@@ -76,7 +76,7 @@ func (d *DB) exciseTable(
 	if err != nil {
 		return nil, nil, err
 	}
-	defer iters.CloseAll()
+	defer func() { _ = iters.CloseAll() }()
 
 	// Create a file to the left of the excise span, if necessary.
 	// The bounds of this file will be [m.Smallest, lastKeyBefore(exciseSpan.Start)].
diff --git a/external_iterator.go b/external_iterator.go
index a0b30556..b88e8cde 100644
--- a/external_iterator.go
+++ b/external_iterator.go
@@ -99,7 +99,7 @@ func NewExternalIterWithContext(
 		dbi.processBounds(iterOpts.LowerBound, iterOpts.UpperBound)
 	}
 	if err := finishInitializingExternal(ctx, dbi); err != nil {
-		dbi.Close()
+		_ = dbi.Close()
 		return nil, err
 	}
 	return dbi, nil
@@ -191,10 +191,10 @@ func createExternalPointIter(
 			}
 			if err != nil {
 				if pointIter != nil {
-					pointIter.Close()
+					_ = pointIter.Close()
 				}
 				for i := range mlevels {
-					mlevels[i].iter.Close()
+					_ = mlevels[i].iter.Close()
 					if mlevels[i].rangeDelIter != nil {
 						mlevels[i].rangeDelIter.Close()
 					}
diff --git a/file_cache.go b/file_cache.go
index f6761f81..50f2bdba 100644
--- a/file_cache.go
+++ b/file_cache.go
@@ -261,7 +261,7 @@ func (h *fileCacheHandle) findOrCreateBlob(
 	valRef, err := h.fileCache.c.FindOrCreate(ctx, key)
 	// TODO(jackson): Propagate a blob metadata object here.
 	if err != nil && IsCorruptionError(err) {
-		h.reportCorruptionFn(nil, err)
+		err = h.reportCorruptionFn(nil, err)
 	}
 	return valRef, err
 }
@@ -571,7 +571,7 @@ func (h *fileCacheHandle) newIters(
 		//   err != nil && iters.point != nil
 		// If it were possible, we'd need to account for it to avoid double
 		// unref-ing here, once during CloseAll and once during `unrefValue`.
-		iters.CloseAll()
+		_ = iters.CloseAll()
 		vRef.Unref()
 		return iterSet{}, err
 	}
diff --git a/ingest.go b/ingest.go
index 307d6a13..632fee4b 100644
--- a/ingest.go
+++ b/ingest.go
@@ -317,7 +317,7 @@ func ingestLoad1(
 	if err != nil {
 		return nil, keyspan.Span{}, err
 	}
-	defer r.Close()
+	defer func() { _ = r.Close() }()
 
 	// Avoid ingesting tables with format versions this DB doesn't support.
 	tf, err := r.TableFormat()
@@ -364,7 +364,7 @@ func ingestLoad1(
 		if err != nil {
 			return nil, keyspan.Span{}, err
 		}
-		defer iter.Close()
+		defer func() { _ = iter.Close() }()
 		var smallest InternalKey
 		if kv := iter.First(); kv != nil {
 			if err := ingestValidateKey(opts, &kv.K); err != nil {
@@ -452,10 +452,14 @@ func ingestLoad1(
 				lastRangeKey = s.Clone()
 			} else {
 				// s == nil.
-				rangeKeyValidator.Validate(nil /* nextFileSmallestKey */)
+				if err := rangeKeyValidator.Validate(nil /* nextFileSmallestKey */); err != nil {
+					return nil, keyspan.Span{}, err
+				}
 			}
 		} else {
-			rangeKeyValidator.Validate(nil /* nextFileSmallestKey */)
+			if err := rangeKeyValidator.Validate(nil /* nextFileSmallestKey */); err != nil {
+				return nil, keyspan.Span{}, err
+			}
 			lastRangeKey = keyspan.Span{}
 		}
 	}
diff --git a/internal/devtools/go.mod b/internal/devtools/go.mod
index 757b7cf7..0911d449 100644
--- a/internal/devtools/go.mod
+++ b/internal/devtools/go.mod
@@ -6,6 +6,7 @@ require (
 	github.com/cockroachdb/cockroach v0.0.0-20250225003441-0fd08b1c5cc1
 	github.com/cockroachdb/crlfmt v0.0.0-20230505164321-461e8663b4b4
 	github.com/jordanlewis/gcassert v0.0.0-20241223220613-778fb585567f
+	github.com/kisielk/errcheck v1.9.0
 	golang.org/x/tools v0.31.0
 	honnef.co/go/tools v0.6.1
 )
diff --git a/internal/devtools/go.sum b/internal/devtools/go.sum
index 24146392..cc8b5681 100644
--- a/internal/devtools/go.sum
+++ b/internal/devtools/go.sum
@@ -29,6 +29,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/jordanlewis/gcassert v0.0.0-20241223220613-778fb585567f h1:h87E/Ysz6iSrYzV20KQPk5mTBqzXmZ7F8P028xciHdE=
 github.com/jordanlewis/gcassert v0.0.0-20241223220613-778fb585567f/go.mod h1:ZybsQk6DWyN5t7An1MuPm1gtSZ1xDaTXS9ZjIOxvQrk=
+github.com/kisielk/errcheck v1.9.0 h1:9xt1zI9EBfcYBvdU1nVrzMzzUPUtPKs9bVSIM3TAb3M=
+github.com/kisielk/errcheck v1.9.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
diff --git a/internal/devtools/roachvet/main.go b/internal/devtools/roachvet/main.go
index 8251aa2f..a3ad7bed 100644
--- a/internal/devtools/roachvet/main.go
+++ b/internal/devtools/roachvet/main.go
@@ -5,15 +5,59 @@
 package main
 
 import (
+	"os"
+
 	"github.com/cockroachdb/cockroach/pkg/testutils/lint/passes/deferloop"
 	"github.com/cockroachdb/cockroach/pkg/testutils/lint/passes/nocopy"
 	"github.com/cockroachdb/cockroach/pkg/testutils/lint/passes/returnerrcheck"
+	"github.com/kisielk/errcheck/errcheck"
 	"golang.org/x/tools/go/analysis/unitchecker"
 )
 
+const errcheckExcludes = `fmt.Fprint
+fmt.Fprintf
+fmt.Fprintln
+(*bufio.Writer).Flush
+(*database/sql.DB).Close
+(*database/sql.Rows).Close
+(*database/sql.Stmt).Close
+(*os.File).Close
+(io.Closer).Close
+(net.Conn).Close
+(*compress/gzip.Reader).Close
+(*strings.Builder).WriteByte
+(*strings.Builder).WriteRune
+(*strings.Builder).WriteString
+(*strings.Builder).Write
+(fmt.State).Write
+crypto/rand.Read
+`
+
+func initErrCheck() (cleanup func()) {
+	// Write the excludes to a temporary files, so we can pass it to errcheck.
+	f, err := os.CreateTemp("", "errcheck-excludes")
+	if err != nil {
+		panic(err)
+	}
+	path := f.Name()
+	if _, err := f.Write([]byte(errcheckExcludes)); err != nil {
+		panic(err)
+	}
+	if err := f.Close(); err != nil {
+		panic(err)
+	}
+	errcheck.Analyzer.Flags.Set("exclude", path)
+	return func() {
+		os.Remove(path)
+	}
+}
+
 func main() {
+	defer initErrCheck()()
+
 	unitchecker.Main(
 		deferloop.Analyzer,
+		errcheck.Analyzer,
 		nocopy.Analyzer,
 		returnerrcheck.Analyzer,
 		ForbiddenImportsAnalyzer,
diff --git a/internal/overlap/checker.go b/internal/overlap/checker.go
index ee4150bc..36ddf6a1 100644
--- a/internal/overlap/checker.go
+++ b/internal/overlap/checker.go
@@ -177,7 +177,7 @@ func (c *Checker) emptyRegionPointsAndRangeDels(
 		return false, err
 	}
 	if points != nil {
-		defer points.Close()
+		defer func() { _ = points.Close() }()
 		var kv *base.InternalKV
 		if c.cmp(region.Start, pointBounds.Start) <= 0 {
 			kv = points.First()
diff --git a/iterator.go b/iterator.go
index bc83f7b0..7c7d99a2 100644
--- a/iterator.go
+++ b/iterator.go
@@ -2753,7 +2753,7 @@ func (i *Iterator) SetOptions(o *IterOptions) {
 	// Iterators created through NewExternalIter have a different iterator
 	// initialization process.
 	if i.externalIter != nil {
-		finishInitializingExternal(i.ctx, i)
+		_ = finishInitializingExternal(i.ctx, i)
 		return
 	}
 	finishInitializingIter(i.ctx, i.alloc)
diff --git a/level_checker.go b/level_checker.go
index 136c8ccd..623fc205 100644
--- a/level_checker.go
+++ b/level_checker.go
@@ -406,7 +406,7 @@ func checkRangeTombstones(c *checkConfig) error {
 			}
 			tombstones, err = addTombstonesFromIter(iters.RangeDeletion(), level, lsmLevel, f.FileNum,
 				tombstones, c.seqNum, c.cmp, c.formatKey)
-			iters.CloseAll()
+			_ = iters.CloseAll()
 
 			if err != nil {
 				return err
diff --git a/lsm_view.go b/lsm_view.go
index b41f8773..7cd13be9 100644
--- a/lsm_view.go
+++ b/lsm_view.go
@@ -196,7 +196,7 @@ func (b *lsmViewBuilder) tableDetails(
 		if err != nil {
 			outf("error opening table: %v", err)
 		} else {
-			defer iters.CloseAll()
+			defer func() { _ = iters.CloseAll() }()
 		}
 	}
 	const maxPoints = 14
diff --git a/metamorphic/build.go b/metamorphic/build.go
index 3a207100..1f6c719d 100644
--- a/metamorphic/build.go
+++ b/metamorphic/build.go
@@ -228,7 +228,7 @@ func buildForIngestExternalEmulation(
 	panicIfErr(err)
 
 	reader, pointIter, rangeDelIter, rangeKeyIter := openExternalObj(t, externalObjID, bounds, syntheticPrefix)
-	defer reader.Close()
+	defer func() { _ = reader.Close() }()
 
 	writable := objstorageprovider.NewFileWritable(f)
 	// The underlying file should already have unique prefixes. Plus we are
diff --git a/metamorphic/ops.go b/metamorphic/ops.go
index def1735e..e5481435 100644
--- a/metamorphic/ops.go
+++ b/metamorphic/ops.go
@@ -765,7 +765,7 @@ func closeIters(
 	rangeKeyIter keyspan.FragmentIterator,
 ) {
 	if pointIter != nil {
-		pointIter.Close()
+		_ = pointIter.Close()
 	}
 	if rangeDelIter != nil {
 		rangeDelIter.Close()
@@ -2173,7 +2173,7 @@ func (r *replicateOp) run(t *Test, h historyRecorder) {
 	if err != nil {
 		panic(err)
 	}
-	defer iter.Close()
+	defer func() { _ = iter.Close() }()
 
 	for ok := iter.SeekGE(r.start); ok && iter.Error() == nil; ok = iter.Next() {
 		hasPoint, hasRange := iter.HasPointAndRange()
diff --git a/metamorphic/options.go b/metamorphic/options.go
index f57e3cb9..e4f1932e 100644
--- a/metamorphic/options.go
+++ b/metamorphic/options.go
@@ -660,7 +660,9 @@ func RandomOptions(
 			fmt.Fprintln(&privateOpts, `  disable_lazy_combined_iteration=true`)
 		}
 		if privateOptsStr := privateOpts.String(); privateOptsStr != `[Options]\n` {
-			parseOptions(testOpts, privateOptsStr, customOptionParsers)
+			if err := parseOptions(testOpts, privateOptsStr, customOptionParsers); err != nil {
+				panic(err)
+			}
 		}
 	}
 
diff --git a/open.go b/open.go
index 74f0bf6b..289336ed 100644
--- a/open.go
+++ b/open.go
@@ -132,7 +132,7 @@ func Open(dirname string, opts *Options) (db *DB, err error) {
 	}
 	defer func() {
 		if db == nil {
-			fileLock.Close()
+			_ = fileLock.Close()
 		}
 	}()
 
@@ -151,7 +151,7 @@ func Open(dirname string, opts *Options) (db *DB, err error) {
 	}
 	defer func() {
 		if db == nil {
-			formatVersionMarker.Close()
+			_ = formatVersionMarker.Close()
 		}
 	}()
 
@@ -195,7 +195,7 @@ func Open(dirname string, opts *Options) (db *DB, err error) {
 	}
 	defer func() {
 		if db == nil {
-			manifestMarker.Close()
+			_ = manifestMarker.Close()
 		}
 	}()
 
@@ -259,7 +259,7 @@ func Open(dirname string, opts *Options) (db *DB, err error) {
 				d.cleanupManager.Close()
 			}
 			if d.objProvider != nil {
-				d.objProvider.Close()
+				_ = d.objProvider.Close()
 			}
 			if r != nil {
 				panic(r)
@@ -392,7 +392,7 @@ func Open(dirname string, opts *Options) (db *DB, err error) {
 	}
 	defer func() {
 		if db == nil {
-			walManager.Close()
+			_ = walManager.Close()
 		}
 	}()
 
@@ -869,7 +869,7 @@ func (d *DB) replayWAL(
 	jobID JobID, ll wal.LogicalLog, strictWALTail bool,
 ) (flushableIngests []*ingestedFlushable, maxSeqNum base.SeqNum, err error) {
 	rr := ll.OpenForRead()
-	defer rr.Close()
+	defer func() { _ = rr.Close() }()
 	var (
 		b               Batch
 		buf             bytes.Buffer
@@ -982,7 +982,9 @@ func (d *DB) replayWAL(
 		// which is used below.
 		b = Batch{}
 		b.db = d
-		b.SetRepr(buf.Bytes())
+		if err := b.SetRepr(buf.Bytes()); err != nil {
+			return nil, 0, err
+		}
 		seqNum := b.SeqNum()
 		maxSeqNum = seqNum + base.SeqNum(b.Count())
 		keysReplayed += int64(b.Count())
diff --git a/record/log_writer.go b/record/log_writer.go
index 6b7b71da..03ffe704 100644
--- a/record/log_writer.go
+++ b/record/log_writer.go
@@ -714,7 +714,7 @@ func (w *LogWriter) flushLoop(context.Context) {
 		if fErr != nil {
 			// NB: pop may invoke ExternalSyncQueueCallback, which is why we have
 			// called f.Unlock() above. We will acquire the lock again below.
-			f.pendingSyncs.pop(snap, fErr)
+			_ = f.pendingSyncs.pop(snap, fErr)
 			// Update the idleStartTime if work could not be done, so that we don't
 			// include the duration we tried to do work as idle. We don't bother
 			// with the rest of the accounting, which means we will undercount.
diff --git a/replay/replay.go b/replay/replay.go
index 90e19c5e..4fcac4b1 100644
--- a/replay/replay.go
+++ b/replay/replay.go
@@ -997,7 +997,7 @@ func loadFlushedSSTableKeys(
 			if err != nil {
 				return err
 			}
-			defer r.Close()
+			defer func() { _ = r.Close() }()
 
 			// Load all the point keys.
 			iter, err := r.NewIter(sstable.NoTransforms, nil, nil,
@@ -1010,7 +1010,7 @@ func loadFlushedSSTableKeys(
 			if err != nil {
 				return err
 			}
-			defer iter.Close()
+			defer func() { _ = iter.Close() }()
 			for kv := iter.First(); kv != nil; kv = iter.Next() {
 				var key flushedKey
 				key.Trailer = kv.K.Trailer
diff --git a/scan_internal.go b/scan_internal.go
index 5bf77bd1..e988b998 100644
--- a/scan_internal.go
+++ b/scan_internal.go
@@ -542,7 +542,7 @@ func (d *DB) truncateSharedFile(
 	if err != nil {
 		return nil, false, err
 	}
-	defer iters.CloseAll()
+	defer func() { _ = iters.CloseAll() }()
 	iter := iters.point
 	rangeDelIter := iters.rangeDeletion
 	rangeKeyIter := iters.rangeKey
@@ -1118,10 +1118,8 @@ func (i *scanInternalIterator) error() error {
 }
 
 // close closes this iterator, and releases any pooled objects.
-func (i *scanInternalIterator) close() error {
-	if err := i.iter.Close(); err != nil {
-		return err
-	}
+func (i *scanInternalIterator) close() {
+	_ = i.iter.Close()
 	if i.readState != nil {
 		i.readState.unref()
 	}
@@ -1152,7 +1150,6 @@ func (i *scanInternalIterator) close() error {
 		iterAllocPool.Put(alloc)
 		i.alloc = nil
 	}
-	return nil
 }
 
 func (i *scanInternalIterator) initializeBoundBufs(lower, upper []byte) {
diff --git a/sstable/blob/blob.go b/sstable/blob/blob.go
index 49b04275..5d5d6eb1 100644
--- a/sstable/blob/blob.go
+++ b/sstable/blob/blob.go
@@ -417,9 +417,9 @@ func NewFileReader(
 	var preallocRH objstorageprovider.PreallocatedReadHandle
 	rh := objstorageprovider.UsePreallocatedReadHandle(
 		r, objstorage.ReadBeforeForNewReader, &preallocRH)
-	defer rh.Close()
 
 	encodedFooter, err := block.ReadRaw(ctx, r, rh, ro.LoggerAndTracer, fileNum, footerBuf[:], off)
+	_ = rh.Close()
 	if err != nil {
 		return nil, err
 	}
diff --git a/sstable/block/block.go b/sstable/block/block.go
index 62118dda..35f05267 100644
--- a/sstable/block/block.go
+++ b/sstable/block/block.go
@@ -150,8 +150,8 @@ func (c *Checksummer) Checksum(block []byte, blockType byte) (checksum uint32) {
 		} else {
 			c.xxHasher.Reset()
 		}
-		c.xxHasher.Write(block)
-		c.xxHasher.Write(c.blockTypeBuf[:])
+		_, _ = c.xxHasher.Write(block)
+		_, _ = c.xxHasher.Write(c.blockTypeBuf[:])
 		checksum = uint32(c.xxHasher.Sum64())
 	default:
 		panic(errors.Newf("unsupported checksum type: %d", c.Type))
diff --git a/sstable/colblk_writer.go b/sstable/colblk_writer.go
index bf06584c..e88cfe95 100644
--- a/sstable/colblk_writer.go
+++ b/sstable/colblk_writer.go
@@ -818,7 +818,9 @@ func (w *RawColumnWriter) finishIndexBlock(rows int) error {
 func (w *RawColumnWriter) flushBufferedIndexBlocks() (rootIndex block.Handle, err error) {
 	// If there's a currently-pending index block, finish it.
 	if w.indexBlock.Rows() > 0 || len(w.indexBuffering.partitions) == 0 {
-		w.finishIndexBlock(w.indexBlock.Rows())
+		if err := w.finishIndexBlock(w.indexBlock.Rows()); err != nil {
+			return block.Handle{}, err
+		}
 	}
 	// We've buffered all the index blocks. Typically there's just one index
 	// block, in which case we're writing a "single-level" index. If we're
@@ -1093,7 +1095,9 @@ func (w *RawColumnWriter) rewriteSuffixes(
 			w.separatorBuf = w.comparer.Successor(w.separatorBuf[:0], blocks[i].end.UserKey)
 			separator = w.separatorBuf
 		}
-		w.enqueuePhysicalBlock(cb, separator)
+		if err := w.enqueuePhysicalBlock(cb, separator); err != nil {
+			return err
+		}
 	}
 
 	if len(blocks) > 0 {
diff --git a/sstable/copier.go b/sstable/copier.go
index 28b996f8..4e3ef4a2 100644
--- a/sstable/copier.go
+++ b/sstable/copier.go
@@ -49,7 +49,7 @@ func CopySpan(
 	o WriterOptions,
 	start, end InternalKey,
 ) (size uint64, _ error) {
-	defer input.Close()
+	defer func() { _ = input.Close() }()
 
 	if r.Properties.NumValueBlocks > 0 || r.Properties.NumRangeKeys() > 0 || r.Properties.NumRangeDeletions > 0 {
 		return copyWholeFileBecauseOfUnsupportedFeature(ctx, input, output) // Finishes/Aborts output.
@@ -73,7 +73,7 @@ func CopySpan(
 
 	defer func() {
 		if w != nil {
-			w.Close()
+			_ = w.Close()
 		}
 	}()
 
@@ -87,7 +87,7 @@ func CopySpan(
 	// and lower-level index blocks with one read.
 	rh := r.blockReader.UsePreallocatedReadHandle(
 		objstorage.ReadBeforeForIndexAndFilter, &preallocRH)
-	defer rh.Close()
+	defer func() { _ = rh.Close() }()
 	rh.SetupForCompaction()
 	indexH, err := r.readTopLevelIndexBlock(ctx, block.NoReadEnv, rh)
 	if err != nil {
@@ -105,7 +105,9 @@ func CopySpan(
 		}
 		filterBytes := append([]byte{}, filterBlock.BlockData()...)
 		filterBlock.Release()
-		w.copyFilter(filterBytes, r.Properties.FilterPolicyName)
+		if err := w.copyFilter(filterBytes, r.Properties.FilterPolicyName); err != nil {
+			return 0, errors.Wrap(err, "copying filter")
+		}
 	}
 
 	// Copy all the props from the source file; we can't compute our own for many
@@ -213,7 +215,7 @@ func intersectingIndexEntries(
 	if err != nil {
 		return nil, err
 	}
-	defer top.Close()
+	defer func() { _ = top.Close() }()
 
 	var alloc bytealloc.A
 	res := make([]indexEntry, 0, r.Properties.NumDataBlocks)
@@ -240,7 +242,7 @@ func intersectingIndexEntries(
 				if err != nil {
 					return err
 				}
-				defer sub.Close()
+				defer func() { _ = sub.Close() }()
 
 				for valid := sub.SeekGE(start.UserKey); valid; valid = sub.Next() {
 					bh, err := sub.BlockHandleWithProperties()
diff --git a/sstable/layout.go b/sstable/layout.go
index e94c6238..be760dfa 100644
--- a/sstable/layout.go
+++ b/sstable/layout.go
@@ -22,6 +22,7 @@ import (
 	"github.com/cockroachdb/pebble/internal/sstableinternal"
 	"github.com/cockroachdb/pebble/internal/treeprinter"
 	"github.com/cockroachdb/pebble/objstorage"
+	"github.com/cockroachdb/pebble/sstable/blob"
 	"github.com/cockroachdb/pebble/sstable/block"
 	"github.com/cockroachdb/pebble/sstable/colblk"
 	"github.com/cockroachdb/pebble/sstable/rowblk"
@@ -209,10 +210,10 @@ func (l *Layout) Describe(
 					return err
 				}
 				if fmtKV == nil {
-					formatting.formatDataBlock(tpNode, r, *b, h.BlockData(), nil)
+					err = formatting.formatDataBlock(tpNode, r, *b, h.BlockData(), nil)
 				} else {
 					var lastKey InternalKey
-					formatting.formatDataBlock(tpNode, r, *b, h.BlockData(), func(key *base.InternalKey, value []byte) string {
+					err = formatting.formatDataBlock(tpNode, r, *b, h.BlockData(), func(key *base.InternalKey, value []byte) string {
 						v := fmtKV(key, value)
 						if base.InternalCompare(r.Comparer.Compare, lastKey, *key) >= 0 {
 							v += " WARNING: OUT OF ORDER KEYS!"
@@ -230,7 +231,7 @@ func (l *Layout) Describe(
 				}
 				// TODO(jackson): colblk ignores fmtKV, because it doesn't
 				// make sense in the context.
-				formatting.formatKeyspanBlock(tpNode, r, *b, h.BlockData(), fmtKV)
+				err = formatting.formatKeyspanBlock(tpNode, r, *b, h.BlockData(), fmtKV)
 
 			case "range-key":
 				h, err = r.readRangeKeyBlock(ctx, block.NoReadEnv, noReadHandle, b.Handle)
@@ -239,14 +240,14 @@ func (l *Layout) Describe(
 				}
 				// TODO(jackson): colblk ignores fmtKV, because it doesn't
 				// make sense in the context.
-				formatting.formatKeyspanBlock(tpNode, r, *b, h.BlockData(), fmtKV)
+				err = formatting.formatKeyspanBlock(tpNode, r, *b, h.BlockData(), fmtKV)
 
 			case "index", "top-index":
 				h, err = r.readIndexBlock(ctx, block.NoReadEnv, noReadHandle, b.Handle)
 				if err != nil {
 					return err
 				}
-				formatting.formatIndexBlock(tpNode, r, *b, h.BlockData())
+				err = formatting.formatIndexBlock(tpNode, r, *b, h.BlockData())
 
 			case "properties":
 				h, err = r.blockReader.Read(ctx, block.NoReadEnv, noReadHandle, b.Handle, noInitBlockMetadataFn)
@@ -378,7 +379,7 @@ func formatColblkIndexBlock(tp treeprinter.Node, r *Reader, b NamedBlockHandle,
 	if err := iter.Init(r.Comparer, data, NoTransforms); err != nil {
 		return err
 	}
-	defer iter.Close()
+	defer func() { _ = iter.Close() }()
 	i := 0
 	for v := iter.First(); v; v = iter.Next() {
 		bh, err := iter.BlockHandleWithProperties()
@@ -409,7 +410,7 @@ func formatColblkDataBlock(
 		if err := iter.Init(&decoder, block.IterTransforms{}); err != nil {
 			return err
 		}
-		defer iter.Close()
+		defer func() { _ = iter.Close() }()
 		for kv := iter.First(); kv != nil; kv = iter.Next() {
 			tp.Child(fmtKV(&kv.K, kv.V.LazyValue().ValueOrHandle))
 		}
@@ -428,8 +429,24 @@ var _ block.GetInternalValueForPrefixAndValueHandler = describingLazyValueHandle
 func (describingLazyValueHandler) GetInternalValueForPrefixAndValueHandle(
 	handle []byte,
 ) base.InternalValue {
-	vh := valblk.DecodeHandle(handle[1:])
-	return base.MakeInPlaceValue([]byte(fmt.Sprintf("value handle %+v", vh)))
+	vp := block.ValuePrefix(handle[0])
+	var result string
+	switch {
+	case vp.IsValueBlockHandle():
+		vh := valblk.DecodeHandle(handle[1:])
+		result = fmt.Sprintf("value handle %+v", vh)
+	case vp.IsBlobValueHandle():
+		handlePreface, remainder := blob.DecodeInlineHandlePreface(handle[1:])
+		handleSuffix := blob.DecodeHandleSuffix(remainder)
+		ih := blob.InlineHandle{
+			InlineHandlePreface: handlePreface,
+			HandleSuffix:        handleSuffix,
+		}
+		result = fmt.Sprintf("blob handle %+v", ih)
+	default:
+		result = "unknown value type"
+	}
+	return base.MakeInPlaceValue([]byte(result))
 }
 
 func formatColblkKeyspanBlock(
diff --git a/sstable/reader.go b/sstable/reader.go
index 0850c701..eb8a1806 100644
--- a/sstable/reader.go
+++ b/sstable/reader.go
@@ -828,7 +828,7 @@ func NewReader(ctx context.Context, f objstorage.Readable, o ReaderOptions) (*Re
 	var preallocRH objstorageprovider.PreallocatedReadHandle
 	rh := objstorageprovider.UsePreallocatedReadHandle(
 		f, objstorage.ReadBeforeForNewReader, &preallocRH)
-	defer rh.Close()
+	defer func() { _ = rh.Close() }()
 
 	footer, err := readFooter(ctx, f, rh, o.LoggerAndTracer, o.CacheOpts.FileNum)
 	if err != nil {
diff --git a/sstable/rowblk/rowblk_fragment_iter.go b/sstable/rowblk/rowblk_fragment_iter.go
index e3178c49..2b4ca597 100644
--- a/sstable/rowblk/rowblk_fragment_iter.go
+++ b/sstable/rowblk/rowblk_fragment_iter.go
@@ -270,7 +270,9 @@ func (i *fragmentIter) gatherBackward(kv *base.InternalKV) (*keyspan.Span, error
 	// Apply a consistent ordering.
 	keyspan.SortKeysByTrailer(i.span.Keys)
 
-	i.applySpanTransforms()
+	if err := i.applySpanTransforms(); err != nil {
+		return nil, err
+	}
 	return &i.span, nil
 }
 
@@ -279,7 +281,7 @@ func (i *fragmentIter) SetContext(ctx context.Context) {}
 
 // Close implements (keyspan.FragmentIterator).Close.
 func (i *fragmentIter) Close() {
-	i.blockIter.Close()
+	_ = i.blockIter.Close()
 	i.closeCheck.Close()
 
 	if invariants.Sometimes(25) {
diff --git a/sstable/suffix_rewriter.go b/sstable/suffix_rewriter.go
index b94694d2..2359da55 100644
--- a/sstable/suffix_rewriter.go
+++ b/sstable/suffix_rewriter.go
@@ -76,7 +76,7 @@ func RewriteKeySuffixesAndReturnFormat(
 	if err != nil {
 		return nil, TableFormatUnspecified, err
 	}
-	defer r.Close()
+	defer func() { _ = r.Close() }()
 	return rewriteKeySuffixesInBlocks(r, sst, out, o, from, to, concurrency)
 }
 
@@ -101,7 +101,7 @@ func rewriteKeySuffixesInBlocks(
 	w := NewRawWriter(out, o)
 	defer func() {
 		if w != nil {
-			w.Close()
+			_ = w.Close()
 		}
 	}()
 
@@ -316,14 +316,14 @@ func RewriteKeySuffixesViaWriter(
 	w := NewRawWriter(out, o)
 	defer func() {
 		if w != nil {
-			w.Close()
+			_ = w.Close()
 		}
 	}()
 	i, err := r.NewIter(NoTransforms, nil, nil, AssertNoBlobHandles)
 	if err != nil {
 		return nil, err
 	}
-	defer i.Close()
+	defer func() { _ = i.Close() }()
 
 	kv := i.First()
 	var scratch InternalKey
diff --git a/sstable/writer.go b/sstable/writer.go
index 4924e652..3396f38a 100644
--- a/sstable/writer.go
+++ b/sstable/writer.go
@@ -95,7 +95,9 @@ func (w *Writer) encodeFragmentedRangeKeySpan(s keyspan.Span) {
 	// to depend on this ordering for colblk-encoded sstables.
 	keyspan.SortKeysByTrailerAndSuffix(w.comparer.CompareRangeSuffixes, s.Keys)
 	if w.Error() == nil {
-		w.rw.EncodeSpan(s)
+		if err := w.rw.EncodeSpan(s); err != nil {
+			w.err = err
+		}
 	}
 }
 
diff --git a/tool/db.go b/tool/db.go
index d4ea2b41..aa4aed90 100644
--- a/tool/db.go
+++ b/tool/db.go
@@ -542,11 +542,11 @@ func (d *dbT) runScan(cmd *cobra.Command, args []string) {
 			}
 			if fmtValues {
 				if needDelimiter {
-					stdout.Write([]byte{' '})
+					_, _ = stdout.Write([]byte{' '})
 				}
 				fmt.Fprintf(stdout, "%s", d.fmtValue.fn(iter.Key(), iter.Value()))
 			}
-			stdout.Write([]byte{'\n'})
+			_, _ = stdout.Write([]byte{'\n'})
 		}
 
 		count++
@@ -651,7 +651,7 @@ This command will remove all keys in this range!`
 	// the database directory so that the command works against any FS.
 	// TODO(radu): remove this if we add a separate DB.Excise method.
 	path := dbOpts.FS.PathJoin(dbDir, fmt.Sprintf("excise-%0x.sst", rand.Uint32()))
-	defer dbOpts.FS.Remove(path)
+	defer func() { _ = dbOpts.FS.Remove(path) }()
 	f, err := dbOpts.FS.Create(path, vfs.WriteCategoryUnspecified)
 	if err != nil {
 		fmt.Fprintf(stderr, "Error creating temporary sst file %q: %s\n", path, err)
@@ -733,7 +733,7 @@ func (d *dbT) runProperties(cmd *cobra.Command, args []string) {
 		if err != nil {
 			return err
 		}
-		defer objProvider.Close()
+		defer func() { _ = objProvider.Close() }()
 
 		// Load and aggregate sstable properties.
 		tw := tabwriter.NewWriter(stdout, 2, 1, 4, ' ', 0)
diff --git a/tool/db_io_bench.go b/tool/db_io_bench.go
index 6a29ba88..7ab28070 100644
--- a/tool/db_io_bench.go
+++ b/tool/db_io_bench.go
@@ -62,7 +62,7 @@ func (d *dbT) runIOBench(cmd *cobra.Command, args []string) {
 
 	defer func() {
 		for _, r := range readables {
-			r.Close()
+			_ = r.Close()
 		}
 	}()
 
@@ -252,7 +252,7 @@ func performIOs(readables []objstorage.Readable, ios []benchIO) error {
 	}
 	defer func() {
 		for i := range rh {
-			rh[i].Close()
+			_ = rh[i].Close()
 		}
 	}()
 
diff --git a/tool/find.go b/tool/find.go
index 72a77d9f..c4b98d85 100644
--- a/tool/find.go
+++ b/tool/find.go
@@ -459,7 +459,7 @@ func (f *findT) searchTables(stdout io.Writer, searchKey []byte, refs []findRef)
 				err = nil
 				return
 			}
-			defer r.Close()
+			defer func() { _ = r.Close() }()
 
 			var transforms sstable.IterTransforms
 			var fragTransforms sstable.FragmentIterTransforms
@@ -475,7 +475,7 @@ func (f *findT) searchTables(stdout io.Writer, searchKey []byte, refs []findRef)
 			if err != nil {
 				return err
 			}
-			defer iter.Close()
+			defer func() { _ = iter.Close() }()
 			kv := iter.SeekGE(searchKey, base.SeekGEFlagsNone)
 
 			// We configured sstable.Reader to return raw tombstones which requires a
diff --git a/tool/sstable.go b/tool/sstable.go
index 97cafc63..6e0ffafa 100644
--- a/tool/sstable.go
+++ b/tool/sstable.go
@@ -315,7 +315,7 @@ func (s *sstableT) runProperties(cmd *cobra.Command, args []string) {
 		for _, key := range keys {
 			fmt.Fprintf(tw, "  %s\t%s\n", key, r.Properties.UserProperties[key])
 		}
-		tw.Flush()
+		_ = tw.Flush()
 	})
 }
 
@@ -536,7 +536,7 @@ func (s *sstableT) foreachSstable(
 			fmt.Fprintf(stderr, "%s: %s\n", path, err)
 			return
 		}
-		defer r.Close()
+		defer func() { _ = r.Close() }()
 		fn(path, r)
 	}
 
diff --git a/tool/util.go b/tool/util.go
index 59fc0b16..ee99c046 100644
--- a/tool/util.go
+++ b/tool/util.go
@@ -278,12 +278,12 @@ func formatKeyValue(
 		needDelimiter := formatKey(w, fmtKey, key)
 		if fmtValue.spec != "null" {
 			if needDelimiter {
-				w.Write([]byte{' '})
+				_, _ = w.Write([]byte{' '})
 			}
 			fmt.Fprintf(w, "%s", fmtValue.fn(key.UserKey, value))
 		}
 	}
-	w.Write([]byte{'\n'})
+	_, _ = w.Write([]byte{'\n'})
 }
 
 func formatSpan(w io.Writer, fmtKey keyFormatter, fmtValue valueFormatter, s *keyspan.Span) {
@@ -297,7 +297,7 @@ func formatSpan(w io.Writer, fmtKey keyFormatter, fmtValue valueFormatter, s *ke
 			case base.InternalKeyKindRangeKeyUnset:
 				fmt.Fprintf(w, ": %s", k.Suffix)
 			}
-			w.Write([]byte{'\n'})
+			_, _ = w.Write([]byte{'\n'})
 		}
 	}
 }
diff --git a/version_set.go b/version_set.go
index 76b392f6..3ca435b3 100644
--- a/version_set.go
+++ b/version_set.go
@@ -904,13 +904,13 @@ func (vs *versionSet) createManifest(
 	)
 	defer func() {
 		if manifest != nil {
-			manifest.Close()
+			_ = manifest.Close()
 		}
 		if manifestFile != nil {
-			manifestFile.Close()
+			_ = manifestFile.Close()
 		}
 		if err != nil {
-			vs.fs.Remove(filename)
+			_ = vs.fs.Remove(filename)
 		}
 	}()
 	manifestFile, err = vs.fs.Create(filename, "pebble-manifest")
@@ -952,7 +952,9 @@ func (vs *versionSet) createManifest(
 	}
 
 	if vs.manifest != nil {
-		vs.manifest.Close()
+		if err := vs.manifest.Close(); err != nil {
+			return err
+		}
 		vs.manifest = nil
 	}
 	if vs.manifestFile != nil {
diff --git a/wal/failover_manager.go b/wal/failover_manager.go
index 6814bcb4..8b95e9da 100644
--- a/wal/failover_manager.go
+++ b/wal/failover_manager.go
@@ -413,7 +413,7 @@ func (m *failoverMonitor) monitorLoop(shouldQuiesce <-chan struct{}) {
 					m.mu.lastFailBackTime = now
 				}
 				if m.mu.writer != nil {
-					m.mu.writer.switchToNewDir(dir)
+					_ = m.mu.writer.switchToNewDir(dir)
 				}
 				m.mu.Unlock()
 			}


Test patch:
diff --git a/data_test.go b/data_test.go
index b0000e2f..49300d23 100644
--- a/data_test.go
+++ b/data_test.go
@@ -642,7 +642,6 @@ func runBuildCmd(
 	if err := sstable.ParseWriterOptions(&writeOpts, td.CmdArgs[1:]...); err != nil {
 		return err
 	}
-	var blobReferences blobtest.References
 	f, err := fs.Create(path, vfs.WriteCategoryUnspecified)
 	if err != nil {
 		return err
@@ -656,8 +655,11 @@ func runBuildCmd(
 		v := kv.InPlaceValue()
 		// If the value looks like it's a debug blob handle, parse it and add it
 		// to the sstable as a blob handle.
-		if ddOpts.blobValues != nil && ddOpts.blobValues.IsBlobHandle(string(v)) {
-			handle, err := ddOpts.blobValues.ParseInlineHandle(string(v), &blobReferences)
+		if blobtest.IsBlobHandle(string(v)) {
+			if ddOpts.blobValues == nil {
+				return errors.Newf("test not set up to support blob handles")
+			}
+			handle, _, err := ddOpts.blobValues.ParseInlineHandle(string(v))
 			if err != nil {
 				return err
 			}
@@ -1085,13 +1087,18 @@ func runDBDefineCmdReuseFS(td *datadriven.TestData, opts *Options) (*DB, error)
 
 	if len(ve.NewTables) > 0 {
 		// Collect any blob files created.
-		err := blobtest.WriteFiles(&valueSeparator.bv, func(fileNum base.DiskFileNum) (objstorage.Writable, error) {
+		fileStats, err := valueSeparator.bv.WriteFiles(func(fileNum base.DiskFileNum) (objstorage.Writable, error) {
 			writable, _, err := d.objProvider.Create(context.Background(), base.FileTypeBlob, fileNum, objstorage.CreateOptions{})
 			return writable, err
-		}, d.opts.MakeBlobWriterOptions(0), valueSeparator.metas)
+		}, d.opts.MakeBlobWriterOptions(0))
 		if err != nil {
 			return nil, err
 		}
+		for f, stats := range fileStats {
+			valueSeparator.metas[f].Size = stats.FileLen
+			valueSeparator.metas[f].ValueSize = stats.UncompressedValueBytes
+		}
+
 		ve.NewBlobFiles = slices.Collect(maps.Values(valueSeparator.metas))
 
 		jobID := d.newJobIDLocked()
diff --git a/ingest_test.go b/ingest_test.go
index d4d3f7f5..b47c1cb1 100644
--- a/ingest_test.go
+++ b/ingest_test.go
@@ -107,7 +107,6 @@ func TestIngestLoad(t *testing.T) {
 				return err.Error()
 			}
 			var bv blobtest.Values
-			var br blobtest.References
 			w := sstable.NewRawWriter(objstorageprovider.NewFileWritable(f), writerOpts)
 			for _, data := range strings.Split(td.Input, "\n") {
 				if strings.HasPrefix(data, "Span: ") {
@@ -124,8 +123,8 @@ func TestIngestLoad(t *testing.T) {
 					return fmt.Sprintf("malformed input: %s\n", data)
 				}
 				key := base.ParseInternalKey(data[:j])
-				if bv.IsBlobHandle(data[j+1:]) {
-					ih, err := bv.ParseInlineHandle(data[j+1:], &br)
+				if blobtest.IsBlobHandle(data[j+1:]) {
+					ih, _, err := bv.ParseInlineHandle(data[j+1:])
 					require.NoError(t, err)
 					if err := w.AddWithBlobHandle(key, ih, base.ShortAttribute(0), false /* forceObsolete */); err != nil {
 						return err.Error()
diff --git a/internal/blobtest/handles.go b/internal/blobtest/handles.go
index 080bf053..ae104b85 100644
--- a/internal/blobtest/handles.go
+++ b/internal/blobtest/handles.go
@@ -15,9 +15,7 @@ import (
 
 	"github.com/cockroachdb/errors"
 	"github.com/cockroachdb/pebble/internal/base"
-	"github.com/cockroachdb/pebble/internal/manifest"
 	"github.com/cockroachdb/pebble/internal/strparse"
-	"github.com/cockroachdb/pebble/internal/testutils"
 	"github.com/cockroachdb/pebble/objstorage"
 	"github.com/cockroachdb/pebble/sstable/blob"
 )
@@ -26,6 +24,8 @@ import (
 // human-readable string describing a blob handle, synthesizing unspecified
 // fields, and tracking the blob handle to support future fetches.
 type Values struct {
+	References References
+
 	mostRecentFileNum base.DiskFileNum
 	mostRecentHandles map[base.DiskFileNum]blob.Handle
 	// trackedHandles maps from a blob handle to its value. The value may be nil
@@ -65,13 +65,17 @@ func (bv *Values) Fetch(
 
 func deriveValueFromHandle(handle blob.Handle) []byte {
 	rng := rand.New(rand.NewPCG((uint64(handle.FileNum)<<32)|uint64(handle.BlockNum), uint64(handle.OffsetInBlock)))
-	return testutils.RandBytes(rng, int(handle.ValueLen))
+	b := make([]byte, handle.ValueLen)
+	for i := range b {
+		b[i] = 'a' + byte(rng.IntN(26))
+	}
+	return b
 }
 
 // ParseInternalValue parses a debug blob handle from the string, returning the
 // handle as an InternalValue and recording the handle's corresponding value.
 func (bv *Values) ParseInternalValue(input string) (base.InternalValue, error) {
-	h, err := bv.Parse(input)
+	h, _, err := bv.Parse(input)
 	if err != nil {
 		return base.InternalValue{}, err
 	}
@@ -100,13 +104,13 @@ func (bv *Values) ParseInternalValue(input string) (base.InternalValue, error) {
 
 // IsBlobHandle returns true if the input string looks like it's a debug blob
 // handle.
-func (bv *Values) IsBlobHandle(input string) bool {
+func IsBlobHandle(input string) bool {
 	return strings.HasPrefix(input, "blob{")
 }
 
 // Parse parses a debug blob handle from the string, returning the handle and
 // recording the handle's corresponding value.
-func (bv *Values) Parse(input string) (h blob.Handle, err error) {
+func (bv *Values) Parse(input string) (h blob.Handle, remaining string, err error) {
 	if bv.trackedHandles == nil {
 		bv.trackedHandles = make(map[blob.Handle]string)
 		bv.mostRecentHandles = make(map[base.DiskFileNum]blob.Handle)
@@ -125,7 +129,7 @@ func (bv *Values) Parse(input string) (h blob.Handle, err error) {
 	var fileNumSet, blockNumSet, offsetSet, valueLenSet bool
 	for done := false; !done; {
 		if p.Done() {
-			return blob.Handle{}, errors.New("unexpected end of input")
+			return blob.Handle{}, "", errors.New("unexpected end of input")
 		}
 		switch x := p.Next(); x {
 		case "}":
@@ -150,10 +154,10 @@ func (bv *Values) Parse(input string) (h blob.Handle, err error) {
 			p.Expect("=")
 			value = p.Next()
 			if valueLenSet && h.ValueLen != uint32(len(value)) {
-				return blob.Handle{}, errors.Newf("valueLen mismatch: %d != %d", h.ValueLen, len(value))
+				return blob.Handle{}, "", errors.Newf("valueLen mismatch: %d != %d", h.ValueLen, len(value))
 			}
 		default:
-			return blob.Handle{}, errors.Newf("unknown field: %q", x)
+			return blob.Handle{}, "", errors.Newf("unknown field: %q", x)
 		}
 	}
 
@@ -176,7 +180,7 @@ func (bv *Values) Parse(input string) (h blob.Handle, err error) {
 	bv.mostRecentFileNum = h.FileNum
 	bv.mostRecentHandles[h.FileNum] = h
 	bv.trackedHandles[h] = value
-	return h, nil
+	return h, p.Remaining(), nil
 }
 
 // ParseInlineHandle parses a debug blob handle from the string. It maps the
@@ -185,40 +189,40 @@ func (bv *Values) Parse(input string) (h blob.Handle, err error) {
 //
 // It's intended for tests that must manually construct inline blob references.
 func (bv *Values) ParseInlineHandle(
-	input string, references *References,
-) (h blob.InlineHandle, err error) {
-	fullHandle, err := bv.Parse(input)
+	input string,
+) (h blob.InlineHandle, remaining string, err error) {
+	fullHandle, remaining, err := bv.Parse(input)
 	if err != nil {
-		return blob.InlineHandle{}, err
+		return blob.InlineHandle{}, "", err
 	}
-	return blob.InlineHandle{
+	h = blob.InlineHandle{
 		InlineHandlePreface: blob.InlineHandlePreface{
-			ReferenceID: references.MapToReferenceID(fullHandle.FileNum),
+			ReferenceID: bv.References.MapToReferenceID(fullHandle.FileNum),
 			ValueLen:    fullHandle.ValueLen,
 		},
 		HandleSuffix: blob.HandleSuffix{
 			BlockNum:      fullHandle.BlockNum,
 			OffsetInBlock: fullHandle.OffsetInBlock,
 		},
-	}, nil
+	}
+	return h, remaining, nil
 }
 
 // WriteFiles writes all the blob files referenced by Values, using
-// newBlobObject to construct new objects. Additionally, it updates the
-// BlobFileMetadatas contained within metas with the resulting physical and
-// logical sizes of the blob files.
-func WriteFiles(
-	bv *Values,
+// newBlobObject to construct new objects.
+//
+// Return the FileWriterStats for the written blob files.
+func (bv *Values) WriteFiles(
 	newBlobObject func(fileNum base.DiskFileNum) (objstorage.Writable, error),
 	writerOpts blob.FileWriterOptions,
-	metas map[base.DiskFileNum]*manifest.BlobFileMetadata,
-) error {
+) (map[base.DiskFileNum]blob.FileWriterStats, error) {
 	// Organize the handles by file number.
 	files := make(map[base.DiskFileNum][]blob.Handle)
 	for handle := range bv.trackedHandles {
 		files[handle.FileNum] = append(files[handle.FileNum], handle)
 	}
 
+	stats := make(map[base.DiskFileNum]blob.FileWriterStats)
 	for fileNum, handles := range files {
 		slices.SortFunc(handles, func(a, b blob.Handle) int {
 			if v := cmp.Compare(a.BlockNum, b.BlockNum); v != 0 {
@@ -228,7 +232,7 @@ func WriteFiles(
 		})
 		writable, err := newBlobObject(fileNum)
 		if err != nil {
-			return err
+			return nil, err
 		}
 		writer := blob.NewFileWriter(fileNum, writable, writerOpts)
 		for i, handle := range handles {
@@ -241,14 +245,13 @@ func WriteFiles(
 				writer.AddValue(deriveValueFromHandle(handle))
 			}
 		}
-		stats, err := writer.Close()
+		fileStats, err := writer.Close()
 		if err != nil {
-			return err
+			return nil, err
 		}
-		metas[fileNum].Size = stats.FileLen
-		metas[fileNum].ValueSize = stats.UncompressedValueBytes
+		stats[fileNum] = fileStats
 	}
-	return nil
+	return stats, nil
 }
 
 // errFromPanic can be used in a recover block to convert panics into errors.
diff --git a/internal/lint/lint_test.go b/internal/lint/lint_test.go
index b9ce6f64..de5ecb28 100644
--- a/internal/lint/lint_test.go
+++ b/internal/lint/lint_test.go
@@ -169,7 +169,18 @@ func TestLint(t *testing.T) {
 				dirCmd(t, pkg.Dir, "go", "vet", "-vettool="+roachVetPath, "./..."),
 				ignoreGoMod(),
 			), func(s string) {
-				t.Errorf("%s", s)
+				if strings.HasPrefix(s, "#") {
+					// Ignore "comments" which show the package before errors. Since we
+					// ignore some errors, we don't want these.
+					return
+				}
+				// Ignore unchecked errors in testing code.
+				if strings.Contains(s, "_test.go:") && strings.Contains(s, "unchecked error") {
+					return
+				}
+				// We add a "\n" so the file name is printed on its own line (which
+				// makes it clickable in Goland),
+				t.Errorf("\n%s", s)
 			}); err != nil {
 			t.Error(err)
 		}
diff --git a/sstable/data_test.go b/sstable/data_test.go
index 2b76a714..c57cd979 100644
--- a/sstable/data_test.go
+++ b/sstable/data_test.go
@@ -16,6 +16,7 @@ import (
 	"github.com/cockroachdb/datadriven"
 	"github.com/cockroachdb/errors"
 	"github.com/cockroachdb/pebble/internal/base"
+	"github.com/cockroachdb/pebble/internal/blobtest"
 	"github.com/cockroachdb/pebble/internal/cache"
 	"github.com/cockroachdb/pebble/internal/keyspan"
 	"github.com/cockroachdb/pebble/internal/sstableinternal"
@@ -54,7 +55,8 @@ func runBuildMemObjCmd(
 			_ = w.Close()
 		}
 	}()
-	if err := ParseTestSST(w, td.Input); err != nil {
+	var bv blobtest.Values
+	if err := ParseTestSST(w, td.Input, &bv); err != nil {
 		return nil, nil, err
 	}
 	if err := w.Close(); err != nil {
diff --git a/sstable/test_utils.go b/sstable/test_utils.go
index 676c55a7..f0ddbbe2 100644
--- a/sstable/test_utils.go
+++ b/sstable/test_utils.go
@@ -6,18 +6,18 @@ package sstable
 
 import (
 	"context"
-	"encoding/hex"
 	"fmt"
 	"runtime/debug"
 	"strconv"
 	"strings"
-	"unicode"
 
 	"github.com/cockroachdb/crlib/crstrings"
 	"github.com/cockroachdb/errors"
 	"github.com/cockroachdb/pebble/bloom"
 	"github.com/cockroachdb/pebble/internal/base"
+	"github.com/cockroachdb/pebble/internal/blobtest"
 	"github.com/cockroachdb/pebble/internal/keyspan"
+	"github.com/cockroachdb/pebble/internal/strparse"
 	"github.com/cockroachdb/pebble/internal/testkeys"
 	"github.com/cockroachdb/pebble/objstorage"
 	"github.com/cockroachdb/pebble/sstable/blob"
@@ -33,12 +33,12 @@ func ReadAll(
 	if err != nil {
 		return nil, nil, nil, err
 	}
-	defer reader.Close()
+	defer func() { _ = reader.Close() }()
 	pointIter, err := reader.NewIter(NoTransforms, nil /* lower */, nil /* upper */, AssertNoBlobHandles)
 	if err != nil {
 		return nil, nil, nil, err
 	}
-	defer pointIter.Close()
+	defer func() { _ = pointIter.Close() }()
 
 	for kv := pointIter.First(); kv != nil; kv = pointIter.Next() {
 		val, _, err := kv.Value(nil)
@@ -127,18 +127,19 @@ func (kv ParsedKVOrSpan) String() string {
 	if !kv.HasBlobValue() {
 		return fmt.Sprintf("%s%s = %s", prefix, kv.Key, kv.Value)
 	}
-	return fmt.Sprintf("%s%s = blobInlineHandle(%d, blk%d, %d, %d, 0x%02x)", prefix, kv.Key,
-		kv.BlobHandle.ReferenceID, kv.BlobHandle.BlockNum, kv.BlobHandle.OffsetInBlock, kv.BlobHandle.ValueLen, kv.Attr,
-	)
+	return fmt.Sprintf("%s%s = blob:%s attr=%d", prefix, kv.Key, kv.BlobHandle, kv.Attr)
 }
 
 // ParseTestKVsAndSpans parses a multi-line string that defines SSTable contents.
-// The lines can be either key-value pairs or key spans.
-// Sample input showing the format:
+//
+// The blobtest.Values argument can be nil if there are no blob references in the input.
+//
+// Each input line can be either a key-value pair or a key spans Sample input
+// showing the format:
 //
 //	a#1,SET = a
 //	force-obsolete: d#2,SET = d
-//	f#3,SET = blobInlineHandle(0, blk1, 10, 100, 0x07)
+//	f#3,SET = blob{fileNum=1 blockNum=2 offset=110 valueLen=200}attr=7
 //	Span: d-e:{(#4,RANGEDEL)}
 //	Span: a-d:{(#11,RANGEKEYSET,@10,foo)}
 //	Span: g-l:{(#5,RANGEDEL)}
@@ -146,7 +147,7 @@ func (kv ParsedKVOrSpan) String() string {
 //
 // Note that the older KV format "<user-key>.<kind>.<seq-num> : <value>" is also supported
 // (for now).
-func ParseTestKVsAndSpans(input string) (_ []ParsedKVOrSpan, err error) {
+func ParseTestKVsAndSpans(input string, bv *blobtest.Values) (_ []ParsedKVOrSpan, err error) {
 	defer func() {
 		if r := recover(); r != nil {
 			err = errors.Newf("%v\n%s", r, debug.Stack())
@@ -162,28 +163,37 @@ func ParseTestKVsAndSpans(input string) (_ []ParsedKVOrSpan, err error) {
 
 		var kv ParsedKVOrSpan
 		line, kv.ForceObsolete = strings.CutPrefix(line, "force-obsolete:")
-		// There should be exactly one "=" or ":" in the remaining line.
-		keyStr, valStr, ok := strings.Cut(line, "=")
-		if !ok {
-			keyStr, valStr, ok = strings.Cut(line, ":")
-		}
-		if !ok {
+		// Cut the key at the first ":" or "=".
+		sepIdx := strings.IndexAny(line, "=:")
+		if sepIdx == -1 {
 			return nil, errors.Newf("KV format is [force-obsolete:] <key>=<value> (or <key>:<value>): %q", line)
 		}
-		kv.Key = base.ParseInternalKey(strings.TrimSpace(keyStr))
-		valStr = strings.TrimSpace(valStr)
+		keyStr := strings.TrimSpace(line[:sepIdx])
+		valStr := strings.TrimSpace(line[sepIdx+1:])
+		kv.Key = base.ParseInternalKey(keyStr)
 
 		if kv.ForceObsolete && kv.Key.Kind() == InternalKeyKindRangeDelete {
 			return nil, errors.Errorf("force-obsolete is not allowed for RANGEDEL")
 		}
 
-		if strings.HasPrefix(valStr, "blobInlineHandle(") {
-			handle, attr, err := decodeBlobInlineHandleAndAttribute(valStr)
+		if blobtest.IsBlobHandle(valStr) {
+			if bv == nil {
+				return nil, errors.Errorf("test not set up to support blob handles")
+			}
+			handle, remaining, err := bv.ParseInlineHandle(valStr)
 			if err != nil {
-				return nil, err
+				return nil, errors.Wrapf(err, "parsing blob handle")
 			}
 			kv.BlobHandle = handle
-			kv.Attr = attr
+			if remaining != "" {
+				p := strparse.MakeParser("=", remaining)
+				p.Expect("attr")
+				p.Expect("=")
+				kv.Attr = base.ShortAttribute(p.Int())
+				if !p.Done() {
+					return nil, errors.Newf("unexpected trailing input %q", p.Remaining())
+				}
+			}
 		} else {
 			kv.Value = []byte(valStr)
 		}
@@ -194,8 +204,10 @@ func ParseTestKVsAndSpans(input string) (_ []ParsedKVOrSpan, err error) {
 
 // ParseTestSST parses the KVs and spans in the input (see ParseTestKVAndSpans)
 // and writes them to an sstable.
-func ParseTestSST(w RawWriter, input string) error {
-	kvs, err := ParseTestKVsAndSpans(input)
+//
+// The blobtest.Values argument can be nil if there are no blob references in the input.
+func ParseTestSST(w RawWriter, input string, bv *blobtest.Values) error {
+	kvs, err := ParseTestKVsAndSpans(input, bv)
 	if err != nil {
 		return err
 	}
@@ -216,51 +228,6 @@ func ParseTestSST(w RawWriter, input string) error {
 	return nil
 }
 
-// decodeBlobInlineHandleAndAttribute decodes a blob handle (in its inline form)
-// and its short attribute from a debug string. It expects a value of the form:
-// blobInlineHandle(<refIndex>, blk<blocknum>, <offset>, <valLen>, <attr>). For example:
-//
-//	blobInlineHandle(24, blk255, 10, 9235, 0x07)
-func decodeBlobInlineHandleAndAttribute(
-	ref string,
-) (blob.InlineHandle, base.ShortAttribute, error) {
-	fields := strings.FieldsFunc(strings.TrimSuffix(strings.TrimPrefix(ref, "blobInlineHandle("), ")"),
-		func(r rune) bool { return r == ',' || unicode.IsSpace(r) })
-	if len(fields) != 5 {
-		return blob.InlineHandle{}, base.ShortAttribute(0), errors.New("expected 5 fields")
-	}
-	refIdx, err := strconv.ParseUint(fields[0], 10, 32)
-	if err != nil {
-		return blob.InlineHandle{}, base.ShortAttribute(0), errors.Wrap(err, "failed to parse file offset")
-	}
-	blockNum, err := strconv.ParseUint(strings.TrimPrefix(fields[1], "blk"), 10, 32)
-	if err != nil {
-		return blob.InlineHandle{}, base.ShortAttribute(0), errors.Wrap(err, "failed to parse block number")
-	}
-	off, err := strconv.ParseUint(fields[2], 10, 32)
-	if err != nil {
-		return blob.InlineHandle{}, base.ShortAttribute(0), errors.Wrap(err, "failed to parse offset")
-	}
-	valLen, err := strconv.ParseUint(fields[3], 10, 32)
-	if err != nil {
-		return blob.InlineHandle{}, base.ShortAttribute(0), errors.Wrap(err, "failed to parse value length")
-	}
-	attr, err := hex.DecodeString(strings.TrimPrefix(fields[4], "0x"))
-	if err != nil {
-		return blob.InlineHandle{}, base.ShortAttribute(0), errors.Wrap(err, "failed to parse attribute")
-	}
-	return blob.InlineHandle{
-		InlineHandlePreface: blob.InlineHandlePreface{
-			ReferenceID: blob.ReferenceID(refIdx),
-			ValueLen:    uint32(valLen),
-		},
-		HandleSuffix: blob.HandleSuffix{
-			BlockNum:      uint32(blockNum),
-			OffsetInBlock: uint32(off),
-		},
-	}, base.ShortAttribute(attr[0]), nil
-}
-
 // ParseWriterOptions modifies WriterOptions based on the given arguments. Each
 // argument is a string or a fmt.Stringer (like datadriven.TestData.CmdArg) with
 // format either "<key>" or "<key>=<value>".
diff --git a/sstable/testdata/writer_blob_value_handles b/sstable/testdata/writer_blob_value_handles
index 8d4240eb..7db251a8 100644
--- a/sstable/testdata/writer_blob_value_handles
+++ b/sstable/testdata/writer_blob_value_handles
@@ -1,9 +1,9 @@
 build table-format=Pebble,v6
-a@2.SET.1:blobInlineHandle(0, blk1, 10, 100, 0x07)
-b@5.SET.7:blobInlineHandle(0, blk2, 110, 200, 0x07)
+a@2.SET.1:blob{fileNum=1 blockNum=1 offset=10 valueLen=10}attr=7
+b@5.SET.7:blob{fileNum=1 blockNum=2 offset=110 valueLen=200}attr=7
 b@4.DEL.3:
-b@3.SET.2:blobInlineHandle(1, blk0, 0, 200, 0x07)
-b@2.SET.1:blobInlineHandle(1, blk1, 294, 103, 0x07)
+b@3.SET.2:blob{fileNum=2 blockNum=0 offset=0 valueLen=200}attr=7
+b@2.SET.1:blob{fileNum=2 blockNum=1 offset=294 valueLen=103}attr=7
 ----
 blob-separated-values: num-values 4
 
@@ -88,7 +88,7 @@ sstable
  │    │    │    │    ├── 109-110: x 11 # data[4] = 17 [128 overall]
  │    │    │    │    └── 110-111: x 17 # data[5] = 23 [134 overall]
  │    │    │    └── data
- │    │    │         ├── 111-116: x 470064010a   # data[0]: "G\x00d\x01\n"
+ │    │    │         ├── 111-116: x 47000a010a   # data[0]: "G\x00\n\x01\n"
  │    │    │         ├── 116-122: x 4700c801026e # data[1]: "G\x00\xc8\x01\x02n"
  │    │    │         ├── 122-122: x              # data[2]:
  │    │    │         ├── 122-128: x 6701c8010000 # data[3]: "g\x01\xc8\x01\x00\x00"
@@ -101,12 +101,12 @@ sstable
  │    │    ├── data for column 6 (bool)
  │    │    │    └── 152-153: x 01 # zero bitmap encoding
  │    │    └── 153-154: x 00 # block padding byte
- │    ├── a@2#1,SET:value handle {ValueLen:0 BlockNum:100 OffsetInBlock:1}
- │    ├── b@5#7,SET:value handle {ValueLen:0 BlockNum:200 OffsetInBlock:2}
+ │    ├── a@2#1,SET:blob handle (f0,blk1[10:20])
+ │    ├── b@5#7,SET:blob handle (f0,blk2[110:310])
  │    ├── b@4#3,DEL:
- │    ├── b@3#2,SET:value handle {ValueLen:1 BlockNum:200 OffsetInBlock:0}
- │    ├── b@2#1,SET:value handle {ValueLen:1 BlockNum:103 OffsetInBlock:1}
- │    └── trailer [compression=none checksum=0x196b9b28]
+ │    ├── b@3#2,SET:blob handle (f1,blk0[0:200])
+ │    ├── b@2#1,SET:blob handle (f1,blk1[294:397])
+ │    └── trailer [compression=none checksum=0x834a75d7]
  ├── index  offset: 159  length: 36
  │    ├── 00000    block:0/154
  │    │   
@@ -134,7 +134,7 @@ sstable
  │    ├── 00547    rocksdb.raw.value.size (15)
  │    ├── restart points
  │    │    └── 00562 [restart 0]
- │    └── trailer [compression=none checksum=0xaaf4ac72]
+ │    └── trailer [compression=none checksum=0x2ff97e5d]
  ├── meta-index  offset: 775  length: 46
  │    ├── 0000    rocksdb.properties block:200/570
  │    │   
diff --git a/sstable/writer_test.go b/sstable/writer_test.go
index 556628ce..67471519 100644
--- a/sstable/writer_test.go
+++ b/sstable/writer_test.go
@@ -22,6 +22,7 @@ import (
 	"github.com/cockroachdb/errors"
 	"github.com/cockroachdb/pebble/bloom"
 	"github.com/cockroachdb/pebble/internal/base"
+	"github.com/cockroachdb/pebble/internal/blobtest"
 	"github.com/cockroachdb/pebble/internal/cache"
 	"github.com/cockroachdb/pebble/internal/humanize"
 	"github.com/cockroachdb/pebble/internal/sstableinternal"
@@ -176,7 +177,8 @@ func runDataDriven(t *testing.T, file string, tableFormat TableFormat) {
 			return ""
 
 		case "write-kvs":
-			if err := ParseTestSST(w, td.Input); err != nil {
+			var bv blobtest.Values
+			if err := ParseTestSST(w, td.Input, &bv); err != nil {
 				return err.Error()
 			}
 			return fmt.Sprintf("EstimatedSize()=%d", w.EstimatedSize())