Skip to content

Commit bf238af

Browse files
xingbowangmeta-codesync[bot]
authored andcommitted
Fix prefix_same_as_start iterator correctness gaps
Summary: Why the change is required: The crash test found prefix_same_as_start iterators returning wrong keys in two ways, both caused by child iterators using prefix filters (SST prefix filter, memtable prefix bloom, LevelIterator prefix exhaustion) for a key in a different prefix than the one the caller iterates: 1. SeekToFirst() without iterate_lower_bound and SeekToLast() without iterate_upper_bound have no seek key to take the prefix from. With a range tombstone that crosses a prefix boundary, MergingIterator reseeks older children to the tombstone boundary key, a prefix filter can drop a child, and the scan can return a wrong first key. Read-path tombstone conversion (min_tombstones_for_range_conversion, EXPERIMENTAL) trusts these scans, so a skipped live key could be covered by a converted tombstone and deleted for good. 2. With prefix_same_as_start and total_order_seek both set, a direction change (Next() after Prev()) can skip keys inside the seek prefix. Under DBOptions::prefix_seek_opt_in_only, total_order_seek is forced on, so every prefix_same_as_start iterator is exposed. Previous behavior: 1. Unbounded SeekToFirst()/SeekToLast() in prefix seek mode were never promised to match total order (the option docs call keys outside the seek prefix undefined, and the Prefix-Seek wiki says these calls are not well supported), but they ran and could return wrong results. 2. Since prefix_seek_opt_in_only was added (9.7.0), prefix_same_as_start and auto_prefix_mode enable prefix filtering even with total_order_seek=true, so for iterators total_order_seek only turns off the implicit prefix seek of default ReadOptions. This was stated in that release note, but not in options.h, where total_order_seek still read as "always total order". Each iterator layer read total_order_seek on its own, and DBIter still treated total_order_seek=true as "the inner iterator is total order". So DBIter::ReverseToForward() skipped its reseek to the current key, and MergingIterator::SwitchToForward() reseeked the other children to the previous key, which can have another prefix. A child without that prefix was dropped by its prefix filter, losing its keys in the seek prefix. New behavior: 1. With DBOptions::prefix_seek_opt_in_only, a prefix_same_as_start iterator leaves itself !Valid() with Status::NotSupported on SeekToFirst() without iterate_lower_bound and SeekToLast() without iterate_upper_bound. The bounded forms still become Seek()/SeekForPrev() and work. Without prefix_seek_opt_in_only, iteration results of these calls are unchanged, but a scan they start under prefix_same_as_start no longer creates read-path range tombstones, until the next Seek()/SeekForPrev(). 2. prefix_same_as_start and auto_prefix_mode take precedence over total_order_seek in every iterator layer. A prefix_same_as_start iterator behaves the same with or without total_order_seek, and so with or without prefix_seek_opt_in_only for Seek()-started scans. Total order and auto_prefix_mode iteration results are not affected. Implementation: - GetIteratorPrefixMode() in options_helper.h decides once how an iterator uses the prefix extractor (kTotalOrder, kAuto, kPrefix), with the order prefix_same_as_start > auto_prefix_mode > total_order_seek > implicit prefix seek. DBIter, the MergingIterator prefix flag, LevelIterator, MemTable, the block-based table filter and hash index, and PlainTable use it instead of reading total_order_seek themselves. Only DBIter's total-order assumption is a correctness fix; for the others the result is unchanged or the prefix_same_as_start + total_order_seek combination gets the same prefix optimizations as plain prefix_same_as_start. - DBIter returns NotSupported from unbounded SeekToFirst()/SeekToLast() for prefix_same_as_start under prefix_seek_opt_in_only. It also remembers when the current scan was started by an unbounded SeekToFirst()/SeekToLast() under prefix_same_as_start, and MaybeInsertRangeTombstone(), the only place that writes a converted tombstone, does nothing then. - options.h: ReadOptions::total_order_seek now starts with its iterator meaning (turns off the implicit prefix seek; prefix_same_as_start and auto_prefix_mode take precedence; forced on by prefix_seek_opt_in_only), auto_prefix_mode notes that prefix_same_as_start takes precedence, and prefix_seek_opt_in_only notes that prefix_same_as_start iterators still use prefix seek. prefix_seek_opt_in_only and prefix_same_as_start document the unbounded SeekToFirst()/SeekToLast() behavior. - Release notes: a behavior change for the NotSupported and conversion change, and a bug fix for the direction change, which also states that prefix_same_as_start now consistently takes precedence over total_order_seek and auto_prefix_mode. - db_stress: new prefix_seek_opt_in_only flag, randomized by db_crashtest.py. TestIterate sometimes sets total_order_seek together with prefix_same_as_start, and VerifyIterator uses GetIteratorPrefixMode(). Under prefix_seek_opt_in_only it checks that unbounded SeekToFirst()/SeekToLast() return NotSupported. Otherwise the prefix_same_as_start SeekToFirst() check only runs when no range tombstone can exist (no DeleteRange, no standalone range deletion ingestion, no read-path conversion), since it would keep hitting the known limitation. Safety, compatibility, and limitations: - No API or format change. Users with prefix_seek_opt_in_only who call unbounded SeekToFirst()/SeekToLast() on a prefix_same_as_start iterator now get NotSupported; they can set a bound, or use total order or auto_prefix_mode. - prefix_same_as_start with total_order_seek or prefix_seek_opt_in_only now reseeks on a direction change, the cost plain prefix_same_as_start already pays. prefix_same_as_start with auto_prefix_mode now uses the SST prefix filter for the seek prefix, as the memtable already did. - Without prefix_seek_opt_in_only, unbounded SeekToFirst()/SeekToLast() under prefix seek can still skip live keys when range tombstones cross prefix boundaries, and unbounded legacy prefix-mode scans have other known holes. These stay documented limitations. Differential Revision: D129901533
1 parent 2d0eacf commit bf238af

18 files changed

Lines changed: 460 additions & 38 deletions

‎db/db_impl/db_impl.cc‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3288,9 +3288,8 @@ InternalIterator* DBImpl::NewInternalIterator(
32883288
// Need to create internal iterator from the arena.
32893289
MergeIteratorBuilder merge_iter_builder(
32903290
&cfd->internal_comparator(), arena,
3291-
// FIXME? It's not clear what interpretation of prefix seek is needed
3292-
// here, and no unit test cares about the value provided here.
3293-
!read_options.total_order_seek && prefix_extractor != nullptr,
3291+
GetIteratorPrefixMode(read_options, prefix_extractor != nullptr) ==
3292+
IteratorPrefixMode::kPrefix,
32943293
read_options.iterate_upper_bound);
32953294
Status s;
32963295
const Comparator* user_comparator = cfd->user_comparator();

‎db/db_iter.cc‎

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,13 @@ DBIter::DBIter(Env* _env, const ReadOptions& read_options,
114114
is_key_seqnum_zero_(false),
115115
prefix_same_as_start_(
116116
prefix_extractor_ ? read_options.prefix_same_as_start : false),
117+
unbounded_scan_not_supported_(prefix_same_as_start_ &&
118+
ioptions.prefix_seek_opt_in_only),
119+
unbounded_prefix_scan_(false),
117120
pin_thru_lifetime_(read_options.pin_data),
118-
expect_total_order_inner_iter_(prefix_extractor_ == nullptr ||
119-
read_options.total_order_seek ||
120-
read_options.auto_prefix_mode),
121+
expect_total_order_inner_iter_(
122+
GetIteratorPrefixMode(read_options, prefix_extractor_ != nullptr) !=
123+
IteratorPrefixMode::kPrefix),
121124
// Read-path range conversion assumes the scan can observe all interior
122125
// live keys. table_filter can hide whole SSTs, read tiers other than
123126
// kReadAllTier can skip SSTs or uncached blocks, and timestamp filtering
@@ -1779,7 +1782,8 @@ void DBIter::FlushPendingTombstoneRun(const Slice& end_key,
17791782
}
17801783

17811784
void DBIter::MaybeInsertRangeTombstone(const Slice& end_key) {
1782-
if (contiguous_tombstone_count_ < min_tombstones_for_range_conversion_) {
1785+
if (contiguous_tombstone_count_ < min_tombstones_for_range_conversion_ ||
1786+
unbounded_prefix_scan_) {
17831787
return;
17841788
}
17851789

@@ -2031,6 +2035,7 @@ void DBIter::Seek(const Slice& target) {
20312035
PERF_COUNTER_ADD(iter_seek_count, 1);
20322036
PERF_CPU_TIMER_GUARD(iter_seek_cpu_nanos, clock_);
20332037
StopWatch sw(clock_, statistics_, DB_SEEK);
2038+
unbounded_prefix_scan_ = false;
20342039

20352040
if (scan_opts_.has_value()) {
20362041
if (scan_opts_->reverse) {
@@ -2147,6 +2152,7 @@ void DBIter::SeekForPrev(const Slice& target) {
21472152
PERF_COUNTER_ADD(iter_seek_count, 1);
21482153
PERF_CPU_TIMER_GUARD(iter_seek_cpu_nanos, clock_);
21492154
StopWatch sw(clock_, statistics_, DB_SEEK);
2155+
unbounded_prefix_scan_ = false;
21502156

21512157
if (scan_opts_.has_value()) {
21522158
if (!scan_opts_->reverse) {
@@ -2272,6 +2278,14 @@ void DBIter::SeekToFirst() {
22722278
Seek(*iterate_lower_bound_);
22732279
return;
22742280
}
2281+
if (unbounded_scan_not_supported_) {
2282+
valid_ = false;
2283+
status_ = Status::NotSupported(
2284+
"SeekToFirst() without iterate_lower_bound is not supported with "
2285+
"prefix_same_as_start under prefix_seek_opt_in_only");
2286+
return;
2287+
}
2288+
unbounded_prefix_scan_ = prefix_same_as_start_;
22752289
PERF_COUNTER_ADD(iter_seek_count, 1);
22762290
PERF_CPU_TIMER_GUARD(iter_seek_cpu_nanos, clock_);
22772291
// Don't use iter_::Seek() if we set a prefix extractor
@@ -2336,6 +2350,14 @@ void DBIter::SeekToLast() {
23362350
#endif
23372351
return;
23382352
}
2353+
if (unbounded_scan_not_supported_) {
2354+
valid_ = false;
2355+
status_ = Status::NotSupported(
2356+
"SeekToLast() without iterate_upper_bound is not supported with "
2357+
"prefix_same_as_start under prefix_seek_opt_in_only");
2358+
return;
2359+
}
2360+
unbounded_prefix_scan_ = prefix_same_as_start_;
23392361

23402362
PERF_COUNTER_ADD(iter_seek_count, 1);
23412363
PERF_CPU_TIMER_GUARD(iter_seek_cpu_nanos, clock_);

‎db/db_iter.h‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,18 @@ class DBIter final : public Iterator {
738738
// user key.
739739
bool is_key_seqnum_zero_;
740740
const bool prefix_same_as_start_;
741+
// prefix_same_as_start_ under DBOptions::prefix_seek_opt_in_only.
742+
// SeekToFirst() without iterate_lower_bound and SeekToLast() without
743+
// iterate_upper_bound set NotSupported: they have no seek key to take the
744+
// prefix from.
745+
const bool unbounded_scan_not_supported_;
746+
// True after SeekToFirst() without iterate_lower_bound or SeekToLast()
747+
// without iterate_upper_bound under prefix_same_as_start, until the next
748+
// Seek() or SeekForPrev(). Such a scan is not limited to one prefix while it
749+
// looks for the first key, but prefix seek in the child iterators can skip
750+
// live keys outside the seek prefix. Read-path range tombstone conversion is
751+
// disabled so that a skipped live key cannot be covered by a new tombstone.
752+
bool unbounded_prefix_scan_;
741753
// Means that we will pin all data blocks we read as long the Iterator
742754
// is not deleted, will be true if ReadOptions::pin_data is true
743755
const bool pin_thru_lifetime_;

‎db/db_iterator_test.cc‎

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,147 @@ class DBIteratorBaseTest : public DBTestBase {
8787
};
8888

8989
#ifdef OS_LINUX
90+
TEST_F(DBIteratorBaseTest, PrefixSameAsStartDirectionChange) {
91+
// prefix_same_as_start uses prefix seek even with total_order_seek=true (as
92+
// forced by prefix_seek_opt_in_only) or auto_prefix_mode. On Next() after
93+
// Prev(), the inner iterator stands on a key with another prefix (bbbb1).
94+
// DBIter must reseek it to the current key; moving it without a reseek
95+
// seeks the other children to bbbb1, where a prefix filter drops the child
96+
// holding kkkk1 and kkkk3.
97+
struct ReadCase {
98+
const char* name;
99+
bool opt_in;
100+
bool total_order_seek;
101+
bool auto_prefix_mode;
102+
};
103+
const std::vector<ReadCase> read_cases = {
104+
{"prefix_same_as_start", false, false, false},
105+
{"+total_order_seek", false, true, false},
106+
{"+auto_prefix_mode", false, false, true},
107+
{"+prefix_seek_opt_in_only", true, false, false},
108+
};
109+
for (bool memtable_bloom : {false, true}) {
110+
for (const ReadCase& rc : read_cases) {
111+
SCOPED_TRACE(std::string(rc.name) +
112+
(memtable_bloom ? " memtable_bloom" : " sst_filter"));
113+
Options options = CurrentOptions();
114+
options.disable_auto_compactions = true;
115+
options.prefix_extractor.reset(NewFixedPrefixTransform(4));
116+
options.prefix_seek_opt_in_only = rc.opt_in;
117+
BlockBasedTableOptions table_options;
118+
if (memtable_bloom) {
119+
options.memtable_prefix_bloom_size_ratio = 0.1;
120+
} else {
121+
table_options.filter_policy.reset(NewBloomFilterPolicy(10, false));
122+
table_options.whole_key_filtering = false;
123+
}
124+
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
125+
DestroyAndReopen(options);
126+
// The child that gets filtered has prefix kkkk only: the SST for the
127+
// SST filter case, the memtable for the memtable bloom case.
128+
const std::vector<std::string> kkkk_only = {"kkkk1", "kkkk3"};
129+
const std::vector<std::string> mixed = {"bbbb1", "kkkk2"};
130+
for (const std::string& key : memtable_bloom ? mixed : kkkk_only) {
131+
ASSERT_OK(Put(key, "v"));
132+
}
133+
ASSERT_OK(Flush());
134+
for (const std::string& key : memtable_bloom ? kkkk_only : mixed) {
135+
ASSERT_OK(Put(key, "v"));
136+
}
137+
138+
ReadOptions ro;
139+
ro.prefix_same_as_start = true;
140+
ro.total_order_seek = rc.total_order_seek;
141+
ro.auto_prefix_mode = rc.auto_prefix_mode;
142+
std::unique_ptr<Iterator> iter(db_->NewIterator(ro));
143+
iter->Seek("kkkk1");
144+
std::vector<std::string> keys;
145+
// Seek, Next, Prev, then Next until the end of the prefix.
146+
for (int step = 0; iter->Valid(); ++step) {
147+
keys.push_back(iter->key().ToString());
148+
if (step == 1) {
149+
iter->Prev();
150+
} else {
151+
iter->Next();
152+
}
153+
}
154+
ASSERT_OK(iter->status());
155+
const std::vector<std::string> expected = {"kkkk1", "kkkk2", "kkkk1",
156+
"kkkk2", "kkkk3"};
157+
EXPECT_EQ(expected, keys);
158+
}
159+
}
160+
}
161+
162+
TEST_F(DBIteratorBaseTest, PrefixSameAsStartWithExplicitBounds) {
163+
// prefix_same_as_start implies a bound at the end of the seek prefix. With
164+
// or without auto_prefix_mode, the iterator returns only keys within both
165+
// that implied bound and the explicit iterate_lower_bound /
166+
// iterate_upper_bound, whichever is narrower.
167+
Options options = CurrentOptions();
168+
options.disable_auto_compactions = true;
169+
options.prefix_extractor.reset(NewFixedPrefixTransform(4));
170+
BlockBasedTableOptions table_options;
171+
table_options.filter_policy.reset(NewBloomFilterPolicy(10, false));
172+
table_options.whole_key_filtering = false;
173+
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
174+
DestroyAndReopen(options);
175+
for (const char* key : {"kkkj9", "kkkk1", "kkkk5"}) {
176+
ASSERT_OK(Put(key, "v"));
177+
}
178+
ASSERT_OK(Flush());
179+
for (const char* key : {"kkkk3", "kkkl1"}) {
180+
ASSERT_OK(Put(key, "v"));
181+
}
182+
183+
struct BoundCase {
184+
const char* name;
185+
const char* bound;
186+
std::vector<std::string> expected;
187+
};
188+
const std::vector<BoundCase> forward_cases = {
189+
{"upper bound inside prefix", "kkkk4", {"kkkk1", "kkkk3"}},
190+
{"upper bound past prefix", "kkkm", {"kkkk1", "kkkk3", "kkkk5"}},
191+
};
192+
const std::vector<BoundCase> backward_cases = {
193+
{"lower bound inside prefix", "kkkk2", {"kkkk5", "kkkk3"}},
194+
{"lower bound before prefix", "kkkj", {"kkkk5", "kkkk3", "kkkk1"}},
195+
};
196+
for (bool auto_prefix_mode : {false, true}) {
197+
ReadOptions ro;
198+
ro.prefix_same_as_start = true;
199+
ro.auto_prefix_mode = auto_prefix_mode;
200+
for (const BoundCase& bc : forward_cases) {
201+
SCOPED_TRACE(std::string(bc.name) +
202+
(auto_prefix_mode ? " auto_prefix_mode" : ""));
203+
Slice upper_bound(bc.bound);
204+
ro.iterate_upper_bound = &upper_bound;
205+
std::unique_ptr<Iterator> iter(db_->NewIterator(ro));
206+
std::vector<std::string> keys;
207+
for (iter->Seek("kkkk1"); iter->Valid(); iter->Next()) {
208+
keys.push_back(iter->key().ToString());
209+
}
210+
ASSERT_OK(iter->status());
211+
EXPECT_EQ(bc.expected, keys);
212+
}
213+
ro.iterate_upper_bound = nullptr;
214+
for (const BoundCase& bc : backward_cases) {
215+
SCOPED_TRACE(std::string(bc.name) +
216+
(auto_prefix_mode ? " auto_prefix_mode" : ""));
217+
Slice lower_bound(bc.bound);
218+
ro.iterate_lower_bound = &lower_bound;
219+
std::unique_ptr<Iterator> iter(db_->NewIterator(ro));
220+
std::vector<std::string> keys;
221+
for (iter->SeekForPrev("kkkk5"); iter->Valid(); iter->Prev()) {
222+
keys.push_back(iter->key().ToString());
223+
}
224+
ASSERT_OK(iter->status());
225+
EXPECT_EQ(bc.expected, keys);
226+
}
227+
ro.iterate_lower_bound = nullptr;
228+
}
229+
}
230+
90231
TEST_F(DBIteratorBaseTest, PrevChecksQueryAbort) {
91232
ASSERT_OK(Put("a", "va"));
92233
ASSERT_OK(Put("b", "vb"));

‎db/db_range_del_test.cc‎

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3165,6 +3165,146 @@ TEST_F(DBRangeDelTest, PrefixSentinelKey) {
31653165
delete iter;
31663166
}
31673167

3168+
namespace {
3169+
Options PrefixFilterOptions(const Options& base) {
3170+
Options options = base;
3171+
options.disable_auto_compactions = true;
3172+
options.prefix_extractor.reset(NewFixedPrefixTransform(4));
3173+
BlockBasedTableOptions table_options;
3174+
table_options.filter_policy.reset(NewBloomFilterPolicy(10, false));
3175+
table_options.whole_key_filtering = false;
3176+
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
3177+
return options;
3178+
}
3179+
3180+
// Scans the whole DB from SeekToFirst() (or SeekToLast() if !forward).
3181+
std::vector<std::string> ScanAllKeys(DB* db, const ReadOptions& ro,
3182+
bool forward) {
3183+
std::vector<std::string> keys;
3184+
std::unique_ptr<Iterator> iter(db->NewIterator(ro));
3185+
if (forward) {
3186+
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
3187+
keys.push_back(iter->key().ToString());
3188+
}
3189+
} else {
3190+
for (iter->SeekToLast(); iter->Valid(); iter->Prev()) {
3191+
keys.push_back(iter->key().ToString());
3192+
}
3193+
}
3194+
EXPECT_OK(iter->status());
3195+
return keys;
3196+
}
3197+
} // namespace
3198+
3199+
// Older data with prefixes aaaa, kkkk and xxxx only; newer tombstones whose
3200+
// boundary keys have prefixes cccc and pppp, which the older data lacks.
3201+
class DBRangeDelCrossPrefixTest : public DBRangeDelTest {
3202+
protected:
3203+
void Populate(const Options& options, int older_level) {
3204+
DestroyAndReopen(options);
3205+
ASSERT_OK(Put("aaaa1", "v"));
3206+
ASSERT_OK(Put("kkkk1", "v"));
3207+
ASSERT_OK(Put("kkkk5", "v"));
3208+
ASSERT_OK(Put("xxxx1", "v"));
3209+
ASSERT_OK(Flush());
3210+
if (older_level > 0) {
3211+
MoveFilesToLevel(older_level);
3212+
ASSERT_EQ(1, NumTableFilesAtLevel(older_level));
3213+
}
3214+
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(),
3215+
"aaaa", "cccc2"));
3216+
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(),
3217+
"pppp", "zzzz"));
3218+
}
3219+
const std::vector<std::string> kLiveKeys = {"kkkk1", "kkkk5"};
3220+
const std::vector<std::string> kLiveKeysReversed = {"kkkk5", "kkkk1"};
3221+
};
3222+
3223+
TEST_F(DBRangeDelCrossPrefixTest, PrefixSameAsStartUnboundedScanNotSupported) {
3224+
Options options = PrefixFilterOptions(CurrentOptions());
3225+
options.prefix_seek_opt_in_only = true;
3226+
Populate(options, /*older_level=*/1);
3227+
3228+
ReadOptions ro;
3229+
ro.prefix_same_as_start = true;
3230+
{
3231+
std::unique_ptr<Iterator> iter(db_->NewIterator(ro));
3232+
iter->SeekToFirst();
3233+
ASSERT_FALSE(iter->Valid());
3234+
ASSERT_TRUE(iter->status().IsNotSupported());
3235+
iter->SeekToLast();
3236+
ASSERT_FALSE(iter->Valid());
3237+
ASSERT_TRUE(iter->status().IsNotSupported());
3238+
// Seek() still works on the same iterator.
3239+
iter->Seek("kkkk");
3240+
ASSERT_OK(iter->status());
3241+
ASSERT_TRUE(iter->Valid());
3242+
ASSERT_EQ("kkkk1", iter->key().ToString());
3243+
}
3244+
// With bounds, SeekToFirst() and SeekToLast() become Seek(lower bound) and
3245+
// SeekForPrev(upper bound).
3246+
const Slice lower_bound("kkkk");
3247+
const Slice upper_bound("kkkk9");
3248+
ro.iterate_lower_bound = &lower_bound;
3249+
ro.iterate_upper_bound = &upper_bound;
3250+
ASSERT_EQ(kLiveKeys, ScanAllKeys(db_.get(), ro, /*forward=*/true));
3251+
ASSERT_EQ(kLiveKeysReversed, ScanAllKeys(db_.get(), ro, /*forward=*/false));
3252+
3253+
// Total order and auto_prefix_mode iterators are not affected.
3254+
ReadOptions total_order;
3255+
ASSERT_EQ(kLiveKeys, ScanAllKeys(db_.get(), total_order, /*forward=*/true));
3256+
ReadOptions auto_prefix;
3257+
auto_prefix.auto_prefix_mode = true;
3258+
ASSERT_EQ(kLiveKeys, ScanAllKeys(db_.get(), auto_prefix, /*forward=*/true));
3259+
}
3260+
3261+
TEST_F(DBRangeDelCrossPrefixTest, AutoPrefixModeFullScanCrossPrefixTombstone) {
3262+
ReadOptions auto_prefix;
3263+
auto_prefix.auto_prefix_mode = true;
3264+
for (int older_level : {0, 1}) {
3265+
SCOPED_TRACE("older_level=" + std::to_string(older_level));
3266+
Populate(PrefixFilterOptions(CurrentOptions()), older_level);
3267+
ASSERT_EQ(kLiveKeys, ScanAllKeys(db_.get(), auto_prefix, /*forward=*/true));
3268+
ASSERT_EQ(kLiveKeysReversed,
3269+
ScanAllKeys(db_.get(), auto_prefix, /*forward=*/false));
3270+
}
3271+
}
3272+
3273+
TEST_F(DBRangeDelTest, PrefixSameAsStartUnboundedScanNoRangeConversion) {
3274+
// An unbounded SeekToFirst() under prefix_same_as_start can skip live keys,
3275+
// so it must not convert point deletes into a range tombstone.
3276+
Options options = PrefixFilterOptions(CurrentOptions());
3277+
options.min_tombstones_for_range_conversion = 2;
3278+
options.statistics = CreateDBStatistics();
3279+
DestroyAndReopen(options);
3280+
ASSERT_OK(Put("kkkk1", "v"));
3281+
ASSERT_OK(Flush());
3282+
ASSERT_OK(Delete("aaaa1"));
3283+
ASSERT_OK(Delete("aaaa2"));
3284+
ASSERT_OK(Delete("aaaa3"));
3285+
3286+
ReadOptions ro;
3287+
ro.prefix_same_as_start = true;
3288+
{
3289+
std::unique_ptr<Iterator> iter(db_->NewIterator(ro));
3290+
iter->SeekToFirst();
3291+
ASSERT_OK(iter->status());
3292+
ASSERT_TRUE(iter->Valid());
3293+
ASSERT_EQ("kkkk1", iter->key().ToString());
3294+
}
3295+
ASSERT_EQ(0, options.statistics->getTickerCount(
3296+
READ_PATH_RANGE_TOMBSTONES_INSERTED));
3297+
// A scan started by Seek() still converts the same deletes.
3298+
{
3299+
std::unique_ptr<Iterator> iter(db_->NewIterator(ro));
3300+
iter->Seek("aaaa");
3301+
ASSERT_OK(iter->status());
3302+
ASSERT_FALSE(iter->Valid());
3303+
}
3304+
ASSERT_EQ(1, options.statistics->getTickerCount(
3305+
READ_PATH_RANGE_TOMBSTONES_INSERTED));
3306+
}
3307+
31683308
TEST_F(DBRangeDelTest, RefreshMemtableIter) {
31693309
Options options = CurrentOptions();
31703310
options.disable_auto_compactions = true;

0 commit comments

Comments
 (0)