diff --git a/sstable/rowblk/rowblk_iter.go b/sstable/rowblk/rowblk_iter.go index ff81a21a4ef..93c728cf16f 100644 --- a/sstable/rowblk/rowblk_iter.go +++ b/sstable/rowblk/rowblk_iter.go @@ -187,6 +187,10 @@ type Iter struct { cachedBuf []byte handle block.BufferHandle // for block iteration for already loaded blocks. + // + // firstUserKey is the user key of the first KV in the block. It has the + // synthetic prefix applied (if any), but does NOT have the synthetic suffix + // applied — the on-disk suffix is preserved. firstUserKey []byte lazyValueHandling struct { getValue block.GetInternalValueForPrefixAndValueHandler @@ -541,6 +545,15 @@ func (i *Iter) cacheEntry() { // IsLowerBound implements the block.DataBlockIterator interface. func (i *Iter) IsLowerBound(k []byte) bool { // Note: we ignore HideObsoletePoints, but false negatives are allowed. + if i.transforms.HasSyntheticSuffix() { + // firstUserKey has the synthetic prefix applied, but its suffix is the + // original on-disk suffix — not the synthetic suffix that the iterator will + // actually return. Instead of applying the synthetic suffix, we + // conservatively require firstUserKey's prefix portion to be strictly + // greater than k (false negatives are allowed). + firstPrefix := i.firstUserKey[:i.split(i.firstUserKey)] + return i.cmp(firstPrefix, k) > 0 + } return i.cmp(i.firstUserKey, k) >= 0 } diff --git a/sstable/rowblk/rowblk_iter_test.go b/sstable/rowblk/rowblk_iter_test.go index 348b6039797..4e66271ceca 100644 --- a/sstable/rowblk/rowblk_iter_test.go +++ b/sstable/rowblk/rowblk_iter_test.go @@ -7,8 +7,11 @@ package rowblk import ( "bytes" "fmt" + "math/rand/v2" + "slices" "strings" "testing" + "time" "unsafe" "github.com/cockroachdb/datadriven" @@ -465,3 +468,76 @@ func TestBlockSyntheticSuffix(t *testing.T) { func ikey(s string) base.InternalKey { return base.InternalKey{UserKey: []byte(s)} } + +func TestIsLowerBoundRand(t *testing.T) { + cmp := testkeys.Comparer.Compare + suffixCmp := testkeys.Comparer.ComparePointSuffixes + split := testkeys.Comparer.Split + + prefixes := []string{"a", "b", "c", "d", "e", "p/", "zzz/"} + suffixes := []string{"", "@1", "@5", "@10", "@20", "@100"} + + seed := uint64(time.Now().UnixNano()) + t.Logf("seed: %d", seed) + rng := rand.New(rand.NewPCG(seed, seed)) + randKey := func() string { + return prefixes[rng.IntN(len(prefixes))] + suffixes[rng.IntN(len(suffixes))] + } + + for iter := 0; iter < 1000; iter++ { + // Construct a block with a random combination of prefixes and suffixes. + n := 1 + rng.IntN(20) + keySet := make(map[string]struct{}, n) + for len(keySet) < n { + keySet[randKey()] = struct{}{} + } + keys := make([]string, 0, n) + for k := range keySet { + keys = append(keys, k) + } + slices.SortFunc(keys, func(a, b string) int { return cmp([]byte(a), []byte(b)) }) + + w := &Writer{RestartInterval: 1 + rng.IntN(4)} + for _, k := range keys { + require.NoError(t, w.Add(ikey(k), nil)) + } + blk := w.Finish() + + // Pick a random prefix and suffix for the transform. + var synthPrefix, synthSuffix []byte + if rand.IntN(2) == 0 { + synthPrefix = []byte(prefixes[rng.IntN(len(prefixes))]) + } + if rand.IntN(2) == 0 { + synthSuffix = []byte(suffixes[rng.IntN(len(suffixes))]) + } + transforms := block.IterTransforms{ + SyntheticPrefixAndSuffix: block.MakeSyntheticPrefixAndSuffix(synthPrefix, synthSuffix), + } + + it, err := NewIter(cmp, suffixCmp, split, blk, transforms) + require.NoError(t, err) + + kv := it.First() + require.NotNil(t, kv) + // firstKey is the key after the transform. + firstKey := slices.Clone(kv.K.UserKey) + + for probe := 0; probe < 50; probe++ { + probeKey := []byte(randKey()) + if rand.IntN(2) == 0 { + // The transformed block keys are random keys with synthPrefix + // prepended. To generate more interesting probe keys, sometimes prepend + // this prefix to the random key. + probeKey = append(slices.Clip(synthPrefix), probeKey...) + } + if it.IsLowerBound(probeKey) { + if cmp(firstKey, probeKey) < 0 { + t.Fatalf("IsLowerBound(%q)=true but First()=%q < probe (transforms=%+v, keys=%v)", + probeKey, firstKey, transforms, keys) + } + } + } + require.NoError(t, it.Close()) + } +}