Repository: cockroachdb/pebble
Pull request #3807 (2024-07-29 21:11:39)

db: remove IterOptions.TableFilter
Remove support for IterOptions.TableFilter. The earliest supported format major version (FormatFlushableIngest introduced in v23.1) guarantees that all sstables that predate the introduction of block-property filters have already been compacted.

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

jbowens: cockroachdb/cockroach#127864 needs to land first.

Patch:
diff --git a/compaction.go b/compaction.go
index 5b84951b..eb27f335 100644
--- a/compaction.go
+++ b/compaction.go
@@ -738,7 +738,7 @@ func (c *compaction) newInputIters(
 			}
 		}
 	} else {
-		addItersForLevel := func(level *compactionLevel, l manifest.Level) error {
+		addItersForLevel := func(level *compactionLevel, l manifest.Layer) error {
 			// Add a *levelIter for point iterators. Because we don't call
 			// initRangeDel, the levelIter will close and forget the range
 			// deletion iterator when it steps on to a new file. Surfacing range
@@ -899,9 +899,9 @@ func (c *compaction) newInputIters(
 }
 
 func (c *compaction) newRangeDelIter(
-	newIters tableNewIters, f manifest.LevelFile, opts IterOptions, l manifest.Level,
+	newIters tableNewIters, f manifest.LevelFile, opts IterOptions, l manifest.Layer,
 ) (*noCloseIter, error) {
-	opts.level = l
+	opts.layer = l
 	iterSet, err := newIters(context.Background(), f.FileMetadata, &opts,
 		internalIterOpts{
 			compaction: true,
diff --git a/compaction_picker.go b/compaction_picker.go
index 273772af..5911c4a1 100644
--- a/compaction_picker.go
+++ b/compaction_picker.go
@@ -130,7 +130,7 @@ func (s sortCompactionLevelsByPriority) Swap(i, j int) {
 // sublevel.
 type sublevelInfo struct {
 	manifest.LevelSlice
-	sublevel manifest.Level
+	sublevel manifest.Layer
 }
 
 func (cl sublevelInfo) Clone() sublevelInfo {
diff --git a/db.go b/db.go
index 4bb30aec..82637466 100644
--- a/db.go
+++ b/db.go
@@ -1475,7 +1475,7 @@ func (i *Iterator) constructPointIter(
 		mlevels = mlevels[:numMergingLevels]
 		levels = levels[:numLevelIters]
 		i.opts.snapshotForHideObsoletePoints = buf.dbi.seqNum
-		addLevelIterForFiles := func(files manifest.LevelIterator, level manifest.Level) {
+		addLevelIterForFiles := func(files manifest.LevelIterator, level manifest.Layer) {
 			li := &levels[levelsIndex]
 
 			li.init(ctx, i.opts, &i.comparer, i.newIters, files, level, internalOpts)
diff --git a/download.go b/download.go
index 3c4a331a..f0f31beb 100644
--- a/download.go
+++ b/download.go
@@ -248,7 +248,7 @@ func (d *DB) newDownloadSpanTask(vers *version, sp DownloadSpan) (_ *downloadSpa
 	// [sp.StartKey, sp.EndKey). Expand the bounds to the left so that we
 	// include the start keys of any external sstables that overlap with
 	// sp.StartKey.
-	vers.IterAllLevelsAndSublevels(func(iter manifest.LevelIterator, level, sublevel int) {
+	vers.IterAllLevelsAndSublevels(func(iter manifest.LevelIterator, level manifest.Layer) {
 		if f := iter.SeekGE(d.cmp, sp.StartKey); f != nil &&
 			objstorage.IsExternalTable(d.objProvider, f.FileBacking.DiskFileNum) &&
 			d.cmp(f.Smallest.UserKey, bounds.Start) < 0 {
diff --git a/external_iterator.go b/external_iterator.go
index 4d81bc71..e667e910 100644
--- a/external_iterator.go
+++ b/external_iterator.go
@@ -108,8 +108,6 @@ func NewExternalIterWithContext(
 
 func validateExternalIterOpts(iterOpts *IterOptions) error {
 	switch {
-	case iterOpts.TableFilter != nil:
-		return errors.Errorf("pebble: external iterator: TableFilter unsupported")
 	case iterOpts.PointKeyFilters != nil:
 		return errors.Errorf("pebble: external iterator: PointKeyFilters unsupported")
 	case iterOpts.RangeKeyFilters != nil:
diff --git a/flushable.go b/flushable.go
index eca37223..c014bb9b 100644
--- a/flushable.go
+++ b/flushable.go
@@ -215,7 +215,7 @@ func (s *ingestedFlushable) newIter(o *IterOptions) internalIterator {
 		opts = *o
 	}
 	return newLevelIter(
-		context.Background(), opts, s.comparer, s.newIters, s.slice.Iter(), manifest.FlushableIngestLevel(),
+		context.Background(), opts, s.comparer, s.newIters, s.slice.Iter(), manifest.FlushableIngestsLayer(),
 		internalIterOpts{},
 	)
 }
@@ -248,7 +248,7 @@ func (s *ingestedFlushable) newRangeDelIter(_ *IterOptions) keyspan.FragmentIter
 	return keyspanimpl.NewLevelIter(
 		context.TODO(),
 		keyspan.SpanIterOptions{}, s.comparer.Compare,
-		s.constructRangeDelIter, s.slice.Iter(), manifest.FlushableIngestLevel(),
+		s.constructRangeDelIter, s.slice.Iter(), manifest.FlushableIngestsLayer(),
 		manifest.KeyTypePoint,
 	)
 }
@@ -262,7 +262,7 @@ func (s *ingestedFlushable) newRangeKeyIter(o *IterOptions) keyspan.FragmentIter
 	return keyspanimpl.NewLevelIter(
 		context.TODO(),
 		keyspan.SpanIterOptions{}, s.comparer.Compare, s.newRangeKeyIters,
-		s.slice.Iter(), manifest.FlushableIngestLevel(), manifest.KeyTypeRange,
+		s.slice.Iter(), manifest.FlushableIngestsLayer(), manifest.KeyTypeRange,
 	)
 }
 
diff --git a/get_iter.go b/get_iter.go
index 511685b0..0410feb0 100644
--- a/get_iter.go
+++ b/get_iter.go
@@ -257,7 +257,7 @@ func (g *getIter) initializeNextIterator() (ok bool) {
 // split user keys across adjacent sstables within a level, ensuring that at
 // most one sstable overlaps g.key.
 func (g *getIter) getSSTableIterators(
-	files manifest.LevelIterator, level manifest.Level,
+	files manifest.LevelIterator, level manifest.Layer,
 ) (internalIterator, keyspan.FragmentIterator, error) {
 	files = files.Filter(manifest.KeyTypePoint)
 	m := files.SeekGE(g.comparer.Compare, g.key)
@@ -270,7 +270,7 @@ func (g *getIter) getSSTableIterators(
 		return emptyIter, nil, nil
 	}
 	// m may possibly contain point (or range deletion) keys relevant to g.key.
-	g.iterOpts.level = level
+	g.iterOpts.layer = level
 	iters, err := g.newIters(context.Background(), m, &g.iterOpts, internalIterOpts{}, iterPointKeys|iterRangeDeletions)
 	if err != nil {
 		return emptyIter, nil, err
diff --git a/ingest.go b/ingest.go
index b29bc02e..d0972d15 100644
--- a/ingest.go
+++ b/ingest.go
@@ -1727,7 +1727,7 @@ func (d *DB) excise(
 				Category: "pebble-ingest",
 				QoSLevel: sstable.LatencySensitiveQoSLevel,
 			},
-			level: manifest.Level(level),
+			layer: manifest.Level(level),
 		}, internalIterOpts{}, iterPointKeys|iterRangeDeletions|iterRangeKeys)
 		itersLoaded = true
 		return err
diff --git a/internal/keyspan/keyspanimpl/level_iter.go b/internal/keyspan/keyspanimpl/level_iter.go
index 2fc5b836..9334a920 100644
--- a/internal/keyspan/keyspanimpl/level_iter.go
+++ b/internal/keyspan/keyspanimpl/level_iter.go
@@ -42,8 +42,8 @@ type TableNewSpanIter func(
 type LevelIter struct {
 	cmp     base.Compare
 	keyType manifest.KeyType
-	// The LSM level this LevelIter is initialized for. Used in logging.
-	level manifest.Level
+	// The LSM level this LevelIter is initialized for.
+	level manifest.Layer
 	// newIter creates a range del iterator if keyType is KeyTypePoint or a range
 	// key iterator if keyType is KeyTypeRange.
 	newIter TableNewSpanIter
@@ -87,7 +87,7 @@ func NewLevelIter(
 	cmp base.Compare,
 	newIter TableNewSpanIter,
 	files manifest.LevelIterator,
-	level manifest.Level,
+	level manifest.Layer,
 	keyType manifest.KeyType,
 ) *LevelIter {
 	l := &LevelIter{}
@@ -105,7 +105,7 @@ func (l *LevelIter) Init(
 	cmp base.Compare,
 	newIter TableNewSpanIter,
 	files manifest.LevelIterator,
-	level manifest.Level,
+	level manifest.Layer,
 	keyType manifest.KeyType,
 ) {
 	if keyType != manifest.KeyTypePoint && keyType != manifest.KeyTypeRange {
diff --git a/internal/manifest/layer.go b/internal/manifest/layer.go
new file mode 100644
index 00000000..12e824d9
--- /dev/null
+++ b/internal/manifest/layer.go
@@ -0,0 +1,121 @@
+// Copyright 2020 The LevelDB-Go and Pebble Authors. All rights reserved. Use
+// of this source code is governed by a BSD-style license that can be found in
+// the LICENSE file.
+
+package manifest
+
+import (
+	"fmt"
+	"math"
+
+	"github.com/cockroachdb/redact"
+)
+
+// Layer represents a section of the logical sstable hierarchy. It can represent:
+//   - a level L1 through L6, or
+//   - the entire L0 level, or
+//   - a specific L0 sublevel, or
+//   - the layer of flushable ingests (which is conceptually above the LSM).
+type Layer struct {
+	kind  layerKind
+	value uint16
+}
+
+// Level returns a Layer that represents an entire level (L0 through L6).
+func Level(level int) Layer {
+	if level < 0 || level >= NumLevels {
+		panic("invalid level")
+	}
+	return Layer{
+		kind:  levelLayer,
+		value: uint16(level),
+	}
+}
+
+// L0Sublevel returns a Layer that represents a specific L0 sublevel.
+func L0Sublevel(sublevel int) Layer {
+	// Note: Pebble stops writes once we get to 1000 sublevels.
+	if sublevel < 0 || sublevel > math.MaxUint16 {
+		panic("invalid sublevel")
+	}
+	return Layer{
+		kind:  l0SublevelLayer,
+		value: uint16(sublevel),
+	}
+}
+
+// FlushableIngestsLayer returns a Layer that represents the flushable ingests
+// layer (which is logically above L0).
+func FlushableIngestsLayer() Layer {
+	return Layer{
+		kind: flushableIngestsLayer,
+	}
+}
+
+// IsSet returns true if l has been initialized.
+func (l Layer) IsSet() bool {
+	return l.kind != 0
+}
+
+// IsFlushableIngests returns true if the layer represents flushable ingests.
+func (l Layer) IsFlushableIngests() bool {
+	return l.kind == flushableIngestsLayer
+}
+
+// IsL0Sublevel returns true if the layer represents an L0 sublevel.
+func (l Layer) IsL0Sublevel() bool {
+	return l.kind == l0SublevelLayer
+}
+
+// Level returns the level for the layer. Must not be called if
+// the layer represents flushable ingests.
+func (l Layer) Level() int {
+	switch l.kind {
+	case levelLayer:
+		return int(l.value)
+	case l0SublevelLayer:
+		return 0
+	case flushableIngestsLayer:
+		panic("flushable ingests layer")
+	default:
+		panic("invalid layer")
+	}
+}
+
+// Sublevel returns the L0 sublevel. Can only be called if the layer represents
+// an L0 sublevel.
+func (l Layer) Sublevel() int {
+	if !l.IsL0Sublevel() {
+		panic("not an L0 sublevel layer")
+	}
+	return int(l.value)
+}
+
+func (l Layer) String() string {
+	switch l.kind {
+	case levelLayer:
+		return fmt.Sprintf("L%d", l.value)
+	case l0SublevelLayer:
+		return fmt.Sprintf("L0.%d", l.value)
+	case flushableIngestsLayer:
+		return "flushable-ingests"
+	default:
+		return "unknown"
+	}
+}
+
+// SafeFormat implements redact.SafeFormatter.
+func (l Layer) SafeFormat(s redact.SafePrinter, verb rune) {
+	s.SafeString(redact.SafeString(l.String()))
+}
+
+type layerKind uint8
+
+const (
+	// Entire level: value contains the level number (0 through 6).
+	levelLayer layerKind = iota + 1
+	// L0 sublevel: value contains the sublevel number.
+	l0SublevelLayer
+	// Flushable ingests layer: value is unused.
+	flushableIngestsLayer
+)
diff --git a/internal/manifest/level.go b/internal/manifest/level.go
deleted file mode 100644
index ce0e854f..00000000
--- a/internal/manifest/level.go
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright 2020 The LevelDB-Go and Pebble Authors. All rights reserved. Use
-// of this source code is governed by a BSD-style license that can be found in
-// the LICENSE file.
-
-package manifest
-
-import "fmt"
-
-const (
-	// 3 bits are necessary to represent level values from 0-6 or 7 for flushable
-	// ingests.
-	levelBits = 3
-	levelMask = (1 << levelBits) - 1
-	// invalidSublevel denotes an invalid or non-applicable sublevel.
-	invalidSublevel           = -1
-	flushableIngestLevelValue = 7
-)
-
-// Level encodes a level and optional sublevel. It can also represent the
-// conceptual layer of flushable ingests as "level" -1.
-//
-// The encoding has the property that Level(0) == L0Sublevel(invalidSublevel).
-type Level uint32
-
-func makeLevel(level, sublevel int) Level {
-	return Level(((sublevel + 1) << levelBits) | level)
-}
-
-// LevelToInt returns the int representation of a Level. Returns -1 if the Level
-// refers to the flushable ingests pseudo-level.
-func LevelToInt(l Level) int {
-	l &= levelMask
-	if l == flushableIngestLevelValue {
-		return -1
-	}
-	return int(l)
-}
-
-// L0Sublevel returns a Level representing the specified L0 sublevel.
-func L0Sublevel(sublevel int) Level {
-	if sublevel < 0 {
-		panic(fmt.Sprintf("invalid L0 sublevel: %d", sublevel))
-	}
-	return makeLevel(0, sublevel)
-}
-
-// FlushableIngestLevel returns a Level that represents the flushable ingests
-// pseudo-level.
-func FlushableIngestLevel() Level {
-	return makeLevel(flushableIngestLevelValue, invalidSublevel)
-}
-
-// FlushableIngestLevel returns true if l represents the flushable ingests
-// pseudo-level.
-func (l Level) FlushableIngestLevel() bool {
-	return LevelToInt(l) == -1
-}
-
-func (l Level) String() string {
-	level := int(l) & levelMask
-	sublevel := (int(l) >> levelBits) - 1
-	if sublevel != invalidSublevel {
-		return fmt.Sprintf("L%d.%d", level, sublevel)
-	}
-	if level == flushableIngestLevelValue {
-		return "flushable-ingest"
-	}
-	return fmt.Sprintf("L%d", level)
-}
diff --git a/internal/manifest/version.go b/internal/manifest/version.go
index a90717e8..81453876 100644
--- a/internal/manifest/version.go
+++ b/internal/manifest/version.go
@@ -1527,12 +1527,12 @@ func (v *Version) Overlaps(level int, bounds base.UserKeyBounds) LevelSlice {
 
 // IterAllLevelsAndSublevels calls fn with an iterator for each L0 sublevel
 // (from top to bottom), then once for each level below L0.
-func (v *Version) IterAllLevelsAndSublevels(fn func(it LevelIterator, level int, sublevel int)) {
+func (v *Version) IterAllLevelsAndSublevels(fn func(it LevelIterator, level Layer)) {
 	for sublevel := len(v.L0SublevelFiles) - 1; sublevel >= 0; sublevel-- {
-		fn(v.L0SublevelFiles[sublevel].Iter(), 0, sublevel)
+		fn(v.L0SublevelFiles[sublevel].Iter(), L0Sublevel(sublevel))
 	}
 	for level := 1; level < NumLevels; level++ {
-		fn(v.Levels[level].Iter(), level, invalidSublevel)
+		fn(v.Levels[level].Iter(), Level(level))
 	}
 }
 
@@ -1620,7 +1620,7 @@ func (l *VersionList) Remove(v *Version) {
 // CheckOrdering checks that the files are consistent with respect to
 // seqnums (for level 0 files -- see detailed comment below) and increasing and non-
 // overlapping internal key ranges (for non-level 0 files).
-func CheckOrdering(cmp Compare, format base.FormatKey, level Level, files LevelIterator) error {
+func CheckOrdering(cmp Compare, format base.FormatKey, level Layer, files LevelIterator) error {
 	// The invariants to check for L0 sublevels are the same as the ones to
 	// check for all other levels. However, if L0 is not organized into
 	// sublevels, or if all L0 files are being passed in, we do the legacy L0
diff --git a/iterator.go b/iterator.go
index 2a2e5ce2..e37de717 100644
--- a/iterator.go
+++ b/iterator.go
@@ -835,10 +835,10 @@ func (i *Iterator) sampleRead() {
 	}
 	if len(mi.levels) > 1 {
 		mi.ForEachLevelIter(func(li *levelIter) (done bool) {
-			if li.level.FlushableIngestLevel() {
+			if li.layer.IsFlushableIngests() {
 				return false
 			}
-			l := manifest.LevelToInt(li.level)
+			l := li.layer.Level()
 			if f := li.iterFile; f != nil {
 				var containsKey bool
 				if i.pos == iterPosNext || i.pos == iterPosCurForward ||
@@ -2579,14 +2579,8 @@ func (i *Iterator) SetOptions(o *IterOptions) {
 	// If OnlyReadGuaranteedDurable changed, the iterator stacks are incorrect,
 	// improperly including or excluding memtables. Invalidate them so that
 	// finishInitializingIter will reconstruct them.
-	//
-	// If either the original options or the new options specify a table filter,
-	// we need to reconstruct the iterator stacks. If they both supply a table
-	// filter, we can't be certain that it's the same filter since we have no
-	// mechanism to compare the filter closures.
 	closeBoth := i.err != nil ||
-		o.OnlyReadGuaranteedDurable != i.opts.OnlyReadGuaranteedDurable ||
-		o.TableFilter != nil || i.opts.TableFilter != nil
+		o.OnlyReadGuaranteedDurable != i.opts.OnlyReadGuaranteedDurable
 
 	// If either options specify block property filters for an iterator stack,
 	// reconstruct it.
diff --git a/level_checker.go b/level_checker.go
index ce53be8f..70668b7c 100644
--- a/level_checker.go
+++ b/level_checker.go
@@ -401,7 +401,7 @@ func checkRangeTombstones(c *checkConfig) error {
 		for f := files.First(); f != nil; f = files.Next() {
 			lf := files.Take()
 			iters, err := c.newIters(
-				context.Background(), lf.FileMetadata, &IterOptions{level: manifest.Level(lsmLevel)},
+				context.Background(), lf.FileMetadata, &IterOptions{layer: manifest.Level(lsmLevel)},
 				internalIterOpts{}, iterRangeDeletions)
 			if err != nil {
 				return err
diff --git a/level_iter.go b/level_iter.go
index 0e0390e2..53da788b 100644
--- a/level_iter.go
+++ b/level_iter.go
@@ -60,8 +60,9 @@ type levelIter struct {
 	// tableOpts.{Lower,Upper}Bound are nil, the corresponding iteration boundary
 	// does not lie within the table bounds.
 	tableOpts IterOptions
-	// The LSM level this levelIter is initialized for.
-	level manifest.Level
+	// The layer this levelIter is initialized for. This can be either
+	// a level L1+, an L0 sublevel, or a flushable ingests layer.
+	layer manifest.Layer
 	// combinedIterState may be set when a levelIter is used during user
 	// iteration. Although levelIter only iterates over point keys, it's also
 	// responsible for lazily constructing the combined range & point iterator
@@ -133,11 +134,11 @@ func newLevelIter(
 	comparer *Comparer,
 	newIters tableNewIters,
 	files manifest.LevelIterator,
-	level manifest.Level,
+	layer manifest.Layer,
 	internalOpts internalIterOpts,
 ) *levelIter {
 	l := &levelIter{}
-	l.init(ctx, opts, comparer, newIters, files, level, internalOpts)
+	l.init(ctx, opts, comparer, newIters, files, layer, internalOpts)
 	return l
 }
 
@@ -147,24 +148,23 @@ func (l *levelIter) init(
 	comparer *Comparer,
 	newIters tableNewIters,
 	files manifest.LevelIterator,
-	level manifest.Level,
+	layer manifest.Layer,
 	internalOpts internalIterOpts,
 ) {
 	l.ctx = ctx
 	l.err = nil
-	l.level = level
+	l.layer = layer
 	l.logger = opts.getLogger()
 	l.prefix = nil
 	l.lower = opts.LowerBound
 	l.upper = opts.UpperBound
-	l.tableOpts.TableFilter = opts.TableFilter
 	l.tableOpts.PointKeyFilters = opts.PointKeyFilters
 	if len(opts.PointKeyFilters) == 0 {
 		l.tableOpts.PointKeyFilters = l.filtersBuf[:0:1]
 	}
 	l.tableOpts.UseL6Filters = opts.UseL6Filters
 	l.tableOpts.CategoryAndQoS = opts.CategoryAndQoS
-	l.tableOpts.level = l.level
+	l.tableOpts.layer = l.layer
 	l.tableOpts.snapshotForHideObsoletePoints = opts.snapshotForHideObsoletePoints
 	l.comparer = comparer
 	l.cmp = comparer.Compare
@@ -619,10 +619,10 @@ func (l *levelIter) verify(kv *base.InternalKV) *base.InternalKV {
 		// bounds as such keys are always range tombstones which will be skipped
 		// by the Iterator.
 		if l.lower != nil && kv != nil && !kv.K.IsExclusiveSentinel() && l.cmp(kv.K.UserKey, l.lower) < 0 {
-			l.logger.Fatalf("levelIter %s: lower bound violation: %s < %s\n%s", l.level, kv, l.lower, debug.Stack())
+			l.logger.Fatalf("levelIter %s: lower bound violation: %s < %s\n%s", l.layer, kv, l.lower, debug.Stack())
 		}
 		if l.upper != nil && kv != nil && !kv.K.IsExclusiveSentinel() && l.cmp(kv.K.UserKey, l.upper) > 0 {
-			l.logger.Fatalf("levelIter %s: upper bound violation: %s > %s\n%s", l.level, kv, l.upper, debug.Stack())
+			l.logger.Fatalf("levelIter %s: upper bound violation: %s > %s\n%s", l.layer, kv, l.upper, debug.Stack())
 		}
 	}
 	return kv
@@ -956,9 +956,9 @@ func (l *levelIter) DebugTree(tp treeprinter.Node) {
 
 func (l *levelIter) String() string {
 	if l.iterFile != nil {
-		return fmt.Sprintf("%s: fileNum=%s", l.level, l.iterFile.FileNum.String())
+		return fmt.Sprintf("%s: fileNum=%s", l.layer, l.iterFile.FileNum.String())
 	}
-	return fmt.Sprintf("%s: fileNum=<nil>", l.level)
+	return fmt.Sprintf("%s: fileNum=<nil>", l.layer)
 }
 
 var _ internalIterator = &levelIter{}
diff --git a/options.go b/options.go
index a7542755..91e46938 100644
--- a/options.go
+++ b/options.go
@@ -115,11 +115,6 @@ type IterOptions struct {
 	// boundary the iterator will return Valid()==false. Setting UpperBound
 	// effectively truncates the key space visible to the iterator.
 	UpperBound []byte
-	// TableFilter can be used to filter the tables that are scanned during
-	// iteration based on the user properties. Return true to scan the table and
-	// false to skip scanning. This function must be thread-safe since the same
-	// function can be used by multiple iterators, if the iterator is cloned.
-	TableFilter func(userProps map[string]string) bool
 	// SkipPoint may be used to skip over point keys that don't match an
 	// arbitrary predicate during iteration. If set, the Iterator invokes
 	// SkipPoint for keys encountered. If SkipPoint returns true, the iterator
@@ -196,9 +191,9 @@ type IterOptions struct {
 	// Internal options.
 
 	logger Logger
-	// Level corresponding to this file. Only passed in if constructed by a
+	// Layer corresponding to this file. Only passed in if constructed by a
 	// levelIter.
-	level manifest.Level
+	layer manifest.Layer
 	// disableLazyCombinedIteration is an internal testing option.
 	disableLazyCombinedIteration bool
 	// snapshotForHideObsoletePoints is specified for/by levelIter when opening
diff --git a/scan_internal.go b/scan_internal.go
index 6081c172..7124600d 100644
--- a/scan_internal.go
+++ b/scan_internal.go
@@ -539,7 +539,7 @@ func (d *DB) truncateSharedFile(
 	iters, err := d.newIters(ctx, file, &IterOptions{
 		LowerBound: lower,
 		UpperBound: upper,
-		level:      manifest.Level(level),
+		layer:      manifest.Level(level),
 	}, internalIterOpts{}, iterPointKeys|iterRangeDeletions|iterRangeKeys)
 	if err != nil {
 		return nil, false, err
@@ -900,7 +900,7 @@ func (i *scanInternalIterator) constructPointIter(
 	rangeDelLevels = rangeDelLevels[:numLevelIters]
 	i.opts.IterOptions.snapshotForHideObsoletePoints = i.seqNum
 	i.opts.IterOptions.CategoryAndQoS = categoryAndQoS
-	addLevelIterForFiles := func(files manifest.LevelIterator, level manifest.Level) {
+	addLevelIterForFiles := func(files manifest.LevelIterator, level manifest.Layer) {
 		li := &levels[levelsIndex]
 		rli := &rangeDelLevels[levelsIndex]
 
diff --git a/table_cache.go b/table_cache.go
index 32071f20..cd3cb225 100644
--- a/table_cache.go
+++ b/table_cache.go
@@ -423,16 +423,10 @@ func (c *tableCacheShard) releaseLoop() {
 // true for ok if this table should be read by this iterator.
 func (c *tableCacheShard) checkAndIntersectFilters(
 	v *tableCacheValue,
-	tableFilter func(userProps map[string]string) bool,
 	blockPropertyFilters []BlockPropertyFilter,
 	boundLimitedFilter sstable.BoundLimitedBlockPropertyFilter,
 	syntheticSuffix sstable.SyntheticSuffix,
 ) (ok bool, filterer *sstable.BlockPropertiesFilterer, err error) {
-	if tableFilter != nil &&
-		!tableFilter(v.reader.Properties.UserProperties) {
-		return false, nil, nil
-	}
-
 	if boundLimitedFilter != nil || len(blockPropertyFilters) > 0 {
 		filterer, err = sstable.IntersectsTable(
 			blockPropertyFilters,
@@ -549,8 +543,8 @@ func (c *tableCacheShard) newPointIter(
 
 		var ok bool
 		var err error
-		ok, filterer, err = c.checkAndIntersectFilters(v, opts.TableFilter,
-			pointKeyFilters, internalOpts.boundLimitedFilter, file.SyntheticSuffix)
+		ok, filterer, err = c.checkAndIntersectFilters(v, pointKeyFilters,
+			internalOpts.boundLimitedFilter, file.SyntheticSuffix)
 		if err != nil {
 			return nil, err
 		} else if !ok {
@@ -565,13 +559,15 @@ func (c *tableCacheShard) newPointIter(
 		// By default, we don't use block filters for L6 and restrict the size for
 		// flushable ingests, as these blocks can be very big.
 		if !opts.UseL6Filters {
-			if opts.level == manifest.Level(6) {
+			if opts.layer == manifest.Level(6) {
 				filterBlockSizeLimit = sstable.NeverUseFilterBlock
-			} else if opts.level.FlushableIngestLevel() {
+			} else if opts.layer.IsFlushableIngests() {
 				filterBlockSizeLimit = filterBlockSizeLimitForFlushableIngests
 			}
 		}
-		ctx = objiotracing.WithLevel(ctx, manifest.LevelToInt(opts.level))
+		if opts.layer.IsSet() && !opts.layer.IsFlushableIngests() {
+			ctx = objiotracing.WithLevel(ctx, opts.layer.Level())
+		}
 	}
 	tableFormat, err := v.reader.TableFormat()
 	if err != nil {
@@ -662,7 +658,7 @@ func (c *tableCacheShard) newRangeKeyIter(
 	// done here, rather than deferring to the block-property collector in order
 	// to maintain parity with point keys and the treatment of RANGEDELs.
 	if v.reader.Properties.NumRangeKeyDels == 0 && len(opts.RangeKeyFilters) > 0 {
-		ok, _, err := c.checkAndIntersectFilters(v, nil, opts.RangeKeyFilters, nil, nil /* TODO(radu) transforms.SyntheticSuffix */)
+		ok, _, err := c.checkAndIntersectFilters(v, opts.RangeKeyFilters, nil, nil /* TODO(radu) transforms.SyntheticSuffix */)
 		if err != nil {
 			return nil, err
 		} else if !ok {


Test patch:
diff --git a/ingest_test.go b/ingest_test.go
index a39ad51a..b8216b9d 100644
--- a/ingest_test.go
+++ b/ingest_test.go
@@ -837,9 +837,9 @@ func TestExcise(t *testing.T) {
 			d.mu.Unlock()
 			current := d.mu.versions.currentVersion()
 
-			current.IterAllLevelsAndSublevels(func(iter manifest.LevelIterator, level, _ int) {
+			current.IterAllLevelsAndSublevels(func(iter manifest.LevelIterator, l manifest.Layer) {
 				for m := iter.SeekGE(d.cmp, exciseSpan.Start); m != nil && d.cmp(m.Smallest.UserKey, exciseSpan.End) < 0; m = iter.Next() {
-					_, err := d.excise(exciseSpan.UserKeyBounds(), m, ve, level)
+					_, err := d.excise(exciseSpan.UserKeyBounds(), m, ve, l.Level())
 					if err != nil {
 						td.Fatalf(t, "error when excising %s: %s", m.FileNum, err.Error())
 					}
diff --git a/internal/keyspan/keyspanimpl/level_iter_test.go b/internal/keyspan/keyspanimpl/level_iter_test.go
index 3ef71a12..decff7ca 100644
--- a/internal/keyspan/keyspanimpl/level_iter_test.go
+++ b/internal/keyspan/keyspanimpl/level_iter_test.go
@@ -317,7 +317,7 @@ func TestLevelIterEquivalence(t *testing.T) {
 			levelIter.Init(
 				context.Background(),
 				keyspan.SpanIterOptions{}, base.DefaultComparer.Compare, tableNewIters,
-				v.Levels[6].Iter(), 0, manifest.KeyTypeRange,
+				v.Levels[6].Iter(), manifest.Level(0), manifest.KeyTypeRange,
 			)
 			levelIters = append(levelIters, &levelIter)
 		}
@@ -428,7 +428,7 @@ func TestLevelIter(t *testing.T) {
 				metas[i] = files[i].meta
 			}
 			lm := manifest.MakeLevelMetadata(cmp, 6, metas)
-			iter := NewLevelIter(context.Background(), keyspan.SpanIterOptions{}, cmp, tableNewIters, lm.Iter(), 6, keyType)
+			iter := NewLevelIter(context.Background(), keyspan.SpanIterOptions{}, cmp, tableNewIters, lm.Iter(), manifest.Level(6), keyType)
 			extraInfo := func() string {
 				return iter.String()
 			}
diff --git a/internal/manifest/level_test.go b/internal/manifest/layer_test.go
similarity index 84%
rename from internal/manifest/level_test.go
rename to internal/manifest/layer_test.go
index 98b726f8..ad1b8a2d 100644
--- a/internal/manifest/level_test.go
+++ b/internal/manifest/layer_test.go
@@ -10,9 +10,9 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-func TestLevel(t *testing.T) {
+func TestLayer(t *testing.T) {
 	testCases := []struct {
-		level    Level
+		layer    Layer
 		expected string
 	}{
 		{Level(0), "L0"},
@@ -27,12 +27,12 @@ func TestLevel(t *testing.T) {
 		{L0Sublevel(1), "L0.1"},
 		{L0Sublevel(2), "L0.2"},
 
-		{FlushableIngestLevel(), "flushable-ingest"},
+		{FlushableIngestsLayer(), "flushable-ingests"},
 	}
 
 	for _, c := range testCases {
 		t.Run(c.expected, func(t *testing.T) {
-			s := c.level.String()
+			s := c.layer.String()
 			require.EqualValues(t, c.expected, s)
 		})
 	}
diff --git a/iterator_test.go b/iterator_test.go
index 651ef208..740a0143 100644
--- a/iterator_test.go
+++ b/iterator_test.go
@@ -1292,13 +1292,9 @@ func TestIteratorBoundsLifetimes(t *testing.T) {
 			return buf.String()
 		case "set-options":
 			var label string
-			var tableFilter bool
 			td.ScanArgs(t, "label", &label)
 			opts := iterators[label].opts
 			for _, arg := range td.CmdArgs {
-				if arg.Key == "table-filter" {
-					tableFilter = true
-				}
 				if arg.Key == "key-types" {
 					switch arg.Vals[0] {
 					case "points-only":
@@ -1313,9 +1309,6 @@ func TestIteratorBoundsLifetimes(t *testing.T) {
 				}
 			}
 			opts.LowerBound, opts.UpperBound = parseBounds(td)
-			if tableFilter {
-				opts.TableFilter = func(userProps map[string]string) bool { return false }
-			}
 			iterators[label].SetOptions(&opts)
 			trashBounds(opts.LowerBound, opts.UpperBound)
 			buf.Reset()
@@ -1542,9 +1535,6 @@ func iterOptionsString(o *IterOptions) string {
 	var buf bytes.Buffer
 	fmt.Fprintf(&buf, "key-types=%s, lower=%q, upper=%q",
 		o.KeyTypes, o.LowerBound, o.UpperBound)
-	if o.TableFilter != nil {
-		fmt.Fprintf(&buf, ", table-filter")
-	}
 	if o.OnlyReadGuaranteedDurable {
 		fmt.Fprintf(&buf, ", only-durable")
 	}