2 * An persistent map : key -> (list of strings), using rocksdb merge.
3 * This file is a test-harness / use-case for the StringAppendOperator.
5 * @author Deon Nicholas (dnicholas@fb.com)
6 * Copyright 2013 Facebook, Inc.
12 #include "rocksdb/db.h"
13 #include "rocksdb/merge_operator.h"
14 #include "rocksdb/utilities/db_ttl.h"
15 #include "test_util/testharness.h"
16 #include "util/random.h"
17 #include "utilities/merge_operators.h"
18 #include "utilities/merge_operators/string_append/stringappend.h"
19 #include "utilities/merge_operators/string_append/stringappend2.h"
21 using namespace ROCKSDB_NAMESPACE
;
23 namespace ROCKSDB_NAMESPACE
{
25 // Path to the database on file system
26 const std::string kDbName
= test::PerThreadDBPath("stringappend_test");
29 // OpenDb opens a (possibly new) rocksdb database with a StringAppendOperator
30 std::shared_ptr
<DB
> OpenNormalDb(char delim_char
) {
33 options
.create_if_missing
= true;
34 options
.merge_operator
.reset(new StringAppendOperator(delim_char
));
35 EXPECT_OK(DB::Open(options
, kDbName
, &db
));
36 return std::shared_ptr
<DB
>(db
);
39 #ifndef ROCKSDB_LITE // TtlDb is not supported in Lite
40 // Open a TtlDB with a non-associative StringAppendTESTOperator
41 std::shared_ptr
<DB
> OpenTtlDb(char delim_char
) {
44 options
.create_if_missing
= true;
45 options
.merge_operator
.reset(new StringAppendTESTOperator(delim_char
));
46 EXPECT_OK(DBWithTTL::Open(options
, kDbName
, &db
, 123456));
47 return std::shared_ptr
<DB
>(db
);
49 #endif // !ROCKSDB_LITE
52 /// StringLists represents a set of string-lists, each with a key-index.
53 /// Supports Append(list, string) and Get(list)
57 //Constructor: specifies the rocksdb db
59 StringLists(std::shared_ptr
<DB
> db
)
66 // Append string val onto the list defined by key; return true on success
67 bool Append(const std::string
& key
, const std::string
& val
){
68 Slice
valSlice(val
.data(), val
.size());
69 auto s
= db_
->Merge(merge_option_
, key
, valSlice
);
74 std::cerr
<< "ERROR " << s
.ToString() << std::endl
;
79 // Returns the list of strings associated with key (or "" if does not exist)
80 bool Get(const std::string
& key
, std::string
* const result
){
81 assert(result
!= nullptr); // we should have a place to store the result
82 auto s
= db_
->Get(get_option_
, key
, result
);
88 // Either key does not exist, or there is some error.
89 *result
= ""; // Always return empty string (just for convention)
91 //NotFound is okay; just return empty (similar to std::map)
92 //But network or db errors, etc, should fail the test (or at least yell)
93 if (!s
.IsNotFound()) {
94 std::cerr
<< "ERROR " << s
.ToString() << std::endl
;
97 // Always return false if s.ok() was not true
103 std::shared_ptr
<DB
> db_
;
104 WriteOptions merge_option_
;
105 ReadOptions get_option_
;
110 // The class for unit-testing
111 class StringAppendOperatorTest
: public testing::Test
{
113 StringAppendOperatorTest() {
114 DestroyDB(kDbName
, Options()); // Start each test with a fresh DB
117 typedef std::shared_ptr
<DB
> (* OpenFuncPtr
)(char);
119 // Allows user to open databases with different configurations.
120 // e.g.: Can open a DB or a TtlDB, etc.
121 static void SetOpenDbFunction(OpenFuncPtr func
) {
126 static OpenFuncPtr OpenDb
;
128 StringAppendOperatorTest::OpenFuncPtr
StringAppendOperatorTest::OpenDb
= nullptr;
130 // THE TEST CASES BEGIN HERE
132 TEST_F(StringAppendOperatorTest
, IteratorTest
) {
133 auto db_
= OpenDb(',');
134 StringLists
slists(db_
);
136 slists
.Append("k1", "v1");
137 slists
.Append("k1", "v2");
138 slists
.Append("k1", "v3");
140 slists
.Append("k2", "a1");
141 slists
.Append("k2", "a2");
142 slists
.Append("k2", "a3");
145 std::unique_ptr
<ROCKSDB_NAMESPACE::Iterator
> it(
146 db_
->NewIterator(ReadOptions()));
147 std::string
k1("k1");
148 std::string
k2("k2");
150 for (it
->Seek(k1
); it
->Valid(); it
->Next()) {
151 res
= it
->value().ToString();
153 ASSERT_EQ(res
, "v1,v2,v3");
156 ASSERT_EQ(res
, "a1,a2,a3");
159 slists
.Append("k2", "a4");
160 slists
.Append("k1", "v4");
162 // Snapshot should still be the same. Should ignore a4 and v4.
164 for (it
->Seek(k1
); it
->Valid(); it
->Next()) {
165 res
= it
->value().ToString();
167 ASSERT_EQ(res
, "v1,v2,v3");
170 ASSERT_EQ(res
, "a1,a2,a3");
175 // Should release the snapshot and be aware of the new stuff now
176 it
.reset(db_
->NewIterator(ReadOptions()));
178 for (it
->Seek(k1
); it
->Valid(); it
->Next()) {
179 res
= it
->value().ToString();
181 ASSERT_EQ(res
, "v1,v2,v3,v4");
184 ASSERT_EQ(res
, "a1,a2,a3,a4");
188 // start from k2 this time.
189 for (it
->Seek(k2
); it
->Valid(); it
->Next()) {
190 res
= it
->value().ToString();
192 ASSERT_EQ(res
, "v1,v2,v3,v4");
195 ASSERT_EQ(res
, "a1,a2,a3,a4");
199 slists
.Append("k3", "g1");
201 it
.reset(db_
->NewIterator(ReadOptions()));
203 std::string
k3("k3");
204 for(it
->Seek(k2
); it
->Valid(); it
->Next()) {
205 res
= it
->value().ToString();
207 ASSERT_EQ(res
, "a1,a2,a3,a4");
210 ASSERT_EQ(res
, "g1");
213 for(it
->Seek(k3
); it
->Valid(); it
->Next()) {
214 res
= it
->value().ToString();
217 ASSERT_EQ(res
, "a1,a2,a3,a4");
220 ASSERT_EQ(res
, "g1");
226 TEST_F(StringAppendOperatorTest
, SimpleTest
) {
227 auto db
= OpenDb(',');
228 StringLists
slists(db
);
230 slists
.Append("k1", "v1");
231 slists
.Append("k1", "v2");
232 slists
.Append("k1", "v3");
235 bool status
= slists
.Get("k1", &res
);
238 ASSERT_EQ(res
, "v1,v2,v3");
241 TEST_F(StringAppendOperatorTest
, SimpleDelimiterTest
) {
242 auto db
= OpenDb('|');
243 StringLists
slists(db
);
245 slists
.Append("k1", "v1");
246 slists
.Append("k1", "v2");
247 slists
.Append("k1", "v3");
250 slists
.Get("k1", &res
);
251 ASSERT_EQ(res
, "v1|v2|v3");
254 TEST_F(StringAppendOperatorTest
, OneValueNoDelimiterTest
) {
255 auto db
= OpenDb('!');
256 StringLists
slists(db
);
258 slists
.Append("random_key", "single_val");
261 slists
.Get("random_key", &res
);
262 ASSERT_EQ(res
, "single_val");
265 TEST_F(StringAppendOperatorTest
, VariousKeys
) {
266 auto db
= OpenDb('\n');
267 StringLists
slists(db
);
269 slists
.Append("c", "asdasd");
270 slists
.Append("a", "x");
271 slists
.Append("b", "y");
272 slists
.Append("a", "t");
273 slists
.Append("a", "r");
274 slists
.Append("b", "2");
275 slists
.Append("c", "asdasd");
279 sa
= slists
.Get("a", &a
);
280 sb
= slists
.Get("b", &b
);
281 sc
= slists
.Get("c", &c
);
283 ASSERT_TRUE(sa
&& sb
&& sc
); // All three keys should have been found
285 ASSERT_EQ(a
, "x\nt\nr");
286 ASSERT_EQ(b
, "y\n2");
287 ASSERT_EQ(c
, "asdasd\nasdasd");
290 // Generate semi random keys/words from a small distribution.
291 TEST_F(StringAppendOperatorTest
, RandomMixGetAppend
) {
292 auto db
= OpenDb(' ');
293 StringLists
slists(db
);
295 // Generate a list of random keys and values
296 const int kWordCount
= 15;
297 std::string words
[] = {"sdasd", "triejf", "fnjsdfn", "dfjisdfsf", "342839",
298 "dsuha", "mabuais", "sadajsid", "jf9834hf", "2d9j89",
299 "dj9823jd", "a", "dk02ed2dh", "$(jd4h984$(*", "mabz"};
300 const int kKeyCount
= 6;
301 std::string keys
[] = {"dhaiusdhu", "denidw", "daisda", "keykey", "muki",
304 // Will store a local copy of all data in order to verify correctness
305 std::map
<std::string
, std::string
> parallel_copy
;
307 // Generate a bunch of random queries (Append and Get)!
308 enum query_t
{ APPEND_OP
, GET_OP
, NUM_OPS
};
309 Random
randomGen(1337); //deterministic seed; always get same results!
311 const int kNumQueries
= 30;
312 for (int q
=0; q
<kNumQueries
; ++q
) {
313 // Generate a random query (Append or Get) and random parameters
314 query_t query
= (query_t
)randomGen
.Uniform((int)NUM_OPS
);
315 std::string key
= keys
[randomGen
.Uniform((int)kKeyCount
)];
316 std::string word
= words
[randomGen
.Uniform((int)kWordCount
)];
318 // Apply the query and any checks.
319 if (query
== APPEND_OP
) {
321 // Apply the rocksdb test-harness Append defined above
322 slists
.Append(key
, word
); //apply the rocksdb append
324 // Apply the similar "Append" to the parallel copy
325 if (parallel_copy
[key
].size() > 0) {
326 parallel_copy
[key
] += " " + word
;
328 parallel_copy
[key
] = word
;
331 } else if (query
== GET_OP
) {
332 // Assumes that a non-existent key just returns <empty>
334 slists
.Get(key
, &res
);
335 ASSERT_EQ(res
, parallel_copy
[key
]);
342 TEST_F(StringAppendOperatorTest
, BIGRandomMixGetAppend
) {
343 auto db
= OpenDb(' ');
344 StringLists
slists(db
);
346 // Generate a list of random keys and values
347 const int kWordCount
= 15;
348 std::string words
[] = {"sdasd", "triejf", "fnjsdfn", "dfjisdfsf", "342839",
349 "dsuha", "mabuais", "sadajsid", "jf9834hf", "2d9j89",
350 "dj9823jd", "a", "dk02ed2dh", "$(jd4h984$(*", "mabz"};
351 const int kKeyCount
= 6;
352 std::string keys
[] = {"dhaiusdhu", "denidw", "daisda", "keykey", "muki",
355 // Will store a local copy of all data in order to verify correctness
356 std::map
<std::string
, std::string
> parallel_copy
;
358 // Generate a bunch of random queries (Append and Get)!
359 enum query_t
{ APPEND_OP
, GET_OP
, NUM_OPS
};
360 Random
randomGen(9138204); // deterministic seed
362 const int kNumQueries
= 1000;
363 for (int q
=0; q
<kNumQueries
; ++q
) {
364 // Generate a random query (Append or Get) and random parameters
365 query_t query
= (query_t
)randomGen
.Uniform((int)NUM_OPS
);
366 std::string key
= keys
[randomGen
.Uniform((int)kKeyCount
)];
367 std::string word
= words
[randomGen
.Uniform((int)kWordCount
)];
369 //Apply the query and any checks.
370 if (query
== APPEND_OP
) {
372 // Apply the rocksdb test-harness Append defined above
373 slists
.Append(key
, word
); //apply the rocksdb append
375 // Apply the similar "Append" to the parallel copy
376 if (parallel_copy
[key
].size() > 0) {
377 parallel_copy
[key
] += " " + word
;
379 parallel_copy
[key
] = word
;
382 } else if (query
== GET_OP
) {
383 // Assumes that a non-existent key just returns <empty>
385 slists
.Get(key
, &res
);
386 ASSERT_EQ(res
, parallel_copy
[key
]);
393 TEST_F(StringAppendOperatorTest
, PersistentVariousKeys
) {
394 // Perform the following operations in limited scope
396 auto db
= OpenDb('\n');
397 StringLists
slists(db
);
399 slists
.Append("c", "asdasd");
400 slists
.Append("a", "x");
401 slists
.Append("b", "y");
402 slists
.Append("a", "t");
403 slists
.Append("a", "r");
404 slists
.Append("b", "2");
405 slists
.Append("c", "asdasd");
412 ASSERT_EQ(a
, "x\nt\nr");
413 ASSERT_EQ(b
, "y\n2");
414 ASSERT_EQ(c
, "asdasd\nasdasd");
417 // Reopen the database (the previous changes should persist / be remembered)
419 auto db
= OpenDb('\n');
420 StringLists
slists(db
);
422 slists
.Append("c", "bbnagnagsx");
423 slists
.Append("a", "sa");
424 slists
.Append("b", "df");
425 slists
.Append("a", "gh");
426 slists
.Append("a", "jk");
427 slists
.Append("b", "l;");
428 slists
.Append("c", "rogosh");
430 // The previous changes should be on disk (L0)
431 // The most recent changes should be in memory (MemTable)
432 // Hence, this will test both Get() paths.
438 ASSERT_EQ(a
, "x\nt\nr\nsa\ngh\njk");
439 ASSERT_EQ(b
, "y\n2\ndf\nl;");
440 ASSERT_EQ(c
, "asdasd\nasdasd\nbbnagnagsx\nrogosh");
443 // Reopen the database (the previous changes should persist / be remembered)
445 auto db
= OpenDb('\n');
446 StringLists
slists(db
);
448 // All changes should be on disk. This will test VersionSet Get()
454 ASSERT_EQ(a
, "x\nt\nr\nsa\ngh\njk");
455 ASSERT_EQ(b
, "y\n2\ndf\nl;");
456 ASSERT_EQ(c
, "asdasd\nasdasd\nbbnagnagsx\nrogosh");
460 TEST_F(StringAppendOperatorTest
, PersistentFlushAndCompaction
) {
461 // Perform the following operations in limited scope
463 auto db
= OpenDb('\n');
464 StringLists
slists(db
);
468 // Append, Flush, Get
469 slists
.Append("c", "asdasd");
470 db
->Flush(ROCKSDB_NAMESPACE::FlushOptions());
471 success
= slists
.Get("c", &c
);
472 ASSERT_TRUE(success
);
473 ASSERT_EQ(c
, "asdasd");
475 // Append, Flush, Append, Get
476 slists
.Append("a", "x");
477 slists
.Append("b", "y");
478 db
->Flush(ROCKSDB_NAMESPACE::FlushOptions());
479 slists
.Append("a", "t");
480 slists
.Append("a", "r");
481 slists
.Append("b", "2");
483 success
= slists
.Get("a", &a
);
484 assert(success
== true);
485 ASSERT_EQ(a
, "x\nt\nr");
487 success
= slists
.Get("b", &b
);
488 assert(success
== true);
489 ASSERT_EQ(b
, "y\n2");
492 success
= slists
.Append("c", "asdasd");
494 success
= slists
.Append("b", "monkey");
497 // I omit the "assert(success)" checks here.
502 ASSERT_EQ(a
, "x\nt\nr");
503 ASSERT_EQ(b
, "y\n2\nmonkey");
504 ASSERT_EQ(c
, "asdasd\nasdasd");
507 // Reopen the database (the previous changes should persist / be remembered)
509 auto db
= OpenDb('\n');
510 StringLists
slists(db
);
513 // Get (Quick check for persistence of previous database)
515 ASSERT_EQ(a
, "x\nt\nr");
517 //Append, Compact, Get
518 slists
.Append("c", "bbnagnagsx");
519 slists
.Append("a", "sa");
520 slists
.Append("b", "df");
521 db
->CompactRange(CompactRangeOptions(), nullptr, nullptr);
525 ASSERT_EQ(a
, "x\nt\nr\nsa");
526 ASSERT_EQ(b
, "y\n2\nmonkey\ndf");
527 ASSERT_EQ(c
, "asdasd\nasdasd\nbbnagnagsx");
530 slists
.Append("a", "gh");
531 slists
.Append("a", "jk");
532 slists
.Append("b", "l;");
533 slists
.Append("c", "rogosh");
537 ASSERT_EQ(a
, "x\nt\nr\nsa\ngh\njk");
538 ASSERT_EQ(b
, "y\n2\nmonkey\ndf\nl;");
539 ASSERT_EQ(c
, "asdasd\nasdasd\nbbnagnagsx\nrogosh");
542 db
->CompactRange(CompactRangeOptions(), nullptr, nullptr);
543 ASSERT_EQ(a
, "x\nt\nr\nsa\ngh\njk");
544 ASSERT_EQ(b
, "y\n2\nmonkey\ndf\nl;");
545 ASSERT_EQ(c
, "asdasd\nasdasd\nbbnagnagsx\nrogosh");
547 // Append, Flush, Compact, Get
548 slists
.Append("b", "afcg");
549 db
->Flush(ROCKSDB_NAMESPACE::FlushOptions());
550 db
->CompactRange(CompactRangeOptions(), nullptr, nullptr);
552 ASSERT_EQ(b
, "y\n2\nmonkey\ndf\nl;\nafcg");
556 TEST_F(StringAppendOperatorTest
, SimpleTestNullDelimiter
) {
557 auto db
= OpenDb('\0');
558 StringLists
slists(db
);
560 slists
.Append("k1", "v1");
561 slists
.Append("k1", "v2");
562 slists
.Append("k1", "v3");
565 bool status
= slists
.Get("k1", &res
);
568 // Construct the desired string. Default constructor doesn't like '\0' chars.
569 std::string
checker("v1,v2,v3"); // Verify that the string is right size.
570 checker
[2] = '\0'; // Use null delimiter instead of comma.
572 assert(checker
.size() == 8); // Verify it is still the correct size
574 // Check that the rocksdb result string matches the desired string
575 assert(res
.size() == checker
.size());
576 ASSERT_EQ(res
, checker
);
579 } // namespace ROCKSDB_NAMESPACE
581 int main(int argc
, char** argv
) {
582 ::testing::InitGoogleTest(&argc
, argv
);
583 // Run with regular database
586 fprintf(stderr
, "Running tests with regular db and operator.\n");
587 StringAppendOperatorTest::SetOpenDbFunction(&OpenNormalDb
);
588 result
= RUN_ALL_TESTS();
591 #ifndef ROCKSDB_LITE // TtlDb is not supported in Lite
594 fprintf(stderr
, "Running tests with ttl db and generic operator.\n");
595 StringAppendOperatorTest::SetOpenDbFunction(&OpenTtlDb
);
596 result
|= RUN_ALL_TESTS();
598 #endif // !ROCKSDB_LITE