1package lsm
2
3import (
4 "bytes"
5 "fmt"
6 "math/rand"
7 "os"
8 "path/filepath"
9 "sort"
10 "testing"
11)
12
13func testOpts() Options {
14 return Options{
15 MemtableBytes: 512,
16 SparseIndexInterval: 4,
17 }
18}
19
20func openTestDB(t *testing.T, opts Options) (*DB, string) {
21 t.Helper()
22 dir := t.TempDir()
23 db, err := Open(dir, opts)
24 if err != nil {
25 t.Fatalf("Open: %v", err)
26 }
27 t.Cleanup(func() { _ = db.Close() })
28 return db, dir
29}
30
31func mustPut(t *testing.T, db *DB, k, v string) {
32 t.Helper()
33 if err := db.Put([]byte(k), []byte(v)); err != nil {
34 t.Fatalf("Put(%q): %v", k, err)
35 }
36}
37
38func mustDelete(t *testing.T, db *DB, k string) {
39 t.Helper()
40 if err := db.Delete([]byte(k)); err != nil {
41 t.Fatalf("Delete(%q): %v", k, err)
42 }
43}
44
45func mustGet(t *testing.T, db *DB, k, want string, wantOK bool) {
46 t.Helper()
47 v, ok, err := db.Get([]byte(k))
48 if err != nil {
49 t.Fatalf("Get(%q): %v", k, err)
50 }
51 if ok != wantOK {
52 t.Fatalf("Get(%q): ok=%v want %v (value=%q)", k, ok, wantOK, v)
53 }
54 if wantOK && string(v) != want {
55 t.Fatalf("Get(%q): got %q want %q", k, v, want)
56 }
57}
58
59func TestPutGetBasic(t *testing.T) {
60 db, _ := openTestDB(t, testOpts())
61 mustPut(t, db, "a", "1")
62 mustPut(t, db, "b", "2")
63 mustGet(t, db, "a", "1", true)
64 mustGet(t, db, "b", "2", true)
65 mustGet(t, db, "c", "", false)
66}
67
68func TestOverwriteDeleteSemantics(t *testing.T) {
69 tests := []struct {
70 name string
71 ops []func(t *testing.T, db *DB)
72 check []struct {
73 key string
74 val string
75 ok bool
76 }
77 }{
78 {
79 name: "overwrite in memtable",
80 ops: []func(t *testing.T, db *DB){
81 func(t *testing.T, db *DB) { mustPut(t, db, "k", "v1") },
82 func(t *testing.T, db *DB) { mustPut(t, db, "k", "v2") },
83 },
84 check: []struct {
85 key, val string
86 ok bool
87 }{
88 {"k", "v2", true},
89 },
90 },
91 {
92 name: "delete in memtable",
93 ops: []func(t *testing.T, db *DB){
94 func(t *testing.T, db *DB) { mustPut(t, db, "k", "v1") },
95 func(t *testing.T, db *DB) { mustDelete(t, db, "k") },
96 },
97 check: []struct {
98 key, val string
99 ok bool
100 }{
101 {"k", "", false},
102 },
103 },
104 {
105 name: "delete then put",
106 ops: []func(t *testing.T, db *DB){
107 func(t *testing.T, db *DB) { mustPut(t, db, "k", "v1") },
108 func(t *testing.T, db *DB) { mustDelete(t, db, "k") },
109 func(t *testing.T, db *DB) { mustPut(t, db, "k", "v2") },
110 },
111 check: []struct {
112 key, val string
113 ok bool
114 }{
115 {"k", "v2", true},
116 },
117 },
118 {
119 name: "overwrite after flush",
120 ops: []func(t *testing.T, db *DB){
121 func(t *testing.T, db *DB) { mustPut(t, db, "k", "v1") },
122 func(t *testing.T, db *DB) {
123 if err := db.Flush(); err != nil {
124 t.Fatal(err)
125 }
126 },
127 func(t *testing.T, db *DB) { mustPut(t, db, "k", "v2") },
128 },
129 check: []struct {
130 key, val string
131 ok bool
132 }{
133 {"k", "v2", true},
134 },
135 },
136 {
137 name: "delete after flush shadows sstable",
138 ops: []func(t *testing.T, db *DB){
139 func(t *testing.T, db *DB) { mustPut(t, db, "k", "v1") },
140 func(t *testing.T, db *DB) {
141 if err := db.Flush(); err != nil {
142 t.Fatal(err)
143 }
144 },
145 func(t *testing.T, db *DB) { mustDelete(t, db, "k") },
146 },
147 check: []struct {
148 key, val string
149 ok bool
150 }{
151 {"k", "", false},
152 },
153 },
154 {
155 name: "delete flushed then flush tombstone",
156 ops: []func(t *testing.T, db *DB){
157 func(t *testing.T, db *DB) { mustPut(t, db, "a", "1") },
158 func(t *testing.T, db *DB) {
159 if err := db.Flush(); err != nil {
160 t.Fatal(err)
161 }
162 },
163 func(t *testing.T, db *DB) { mustDelete(t, db, "a") },
164 func(t *testing.T, db *DB) {
165 if err := db.Flush(); err != nil {
166 t.Fatal(err)
167 }
168 },
169 },
170 check: []struct {
171 key, val string
172 ok bool
173 }{
174 {"a", "", false},
175 },
176 },
177 {
178 name: "multiple keys overwrite mix",
179 ops: []func(t *testing.T, db *DB){
180 func(t *testing.T, db *DB) {
181 mustPut(t, db, "a", "1")
182 mustPut(t, db, "b", "2")
183 mustPut(t, db, "c", "3")
184 },
185 func(t *testing.T, db *DB) {
186 if err := db.Flush(); err != nil {
187 t.Fatal(err)
188 }
189 },
190 func(t *testing.T, db *DB) {
191 mustPut(t, db, "b", "22")
192 mustDelete(t, db, "c")
193 mustPut(t, db, "d", "4")
194 },
195 },
196 check: []struct {
197 key, val string
198 ok bool
199 }{
200 {"a", "1", true},
201 {"b", "22", true},
202 {"c", "", false},
203 {"d", "4", true},
204 },
205 },
206 }
207
208 for _, tc := range tests {
209 t.Run(tc.name, func(t *testing.T) {
210 db, _ := openTestDB(t, testOpts())
211 for _, op := range tc.ops {
212 op(t, db)
213 }
214 for _, c := range tc.check {
215 mustGet(t, db, c.key, c.val, c.ok)
216 }
217 })
218 }
219}
220
221func TestWALCrashRecovery(t *testing.T) {
222 tests := []struct {
223 name string
224 ops func(t *testing.T, db *DB)
225 want map[string]string
226 }{
227 {
228 name: "puts survive reopen without close flush",
229 ops: func(t *testing.T, db *DB) {
230 mustPut(t, db, "x", "1")
231 mustPut(t, db, "y", "2")
232 },
233 want: map[string]string{"x": "1", "y": "2"},
234 },
235 {
236 name: "overwrite and delete in WAL",
237 ops: func(t *testing.T, db *DB) {
238 mustPut(t, db, "a", "old")
239 mustPut(t, db, "a", "new")
240 mustPut(t, db, "b", "bye")
241 mustDelete(t, db, "b")
242 mustPut(t, db, "c", "keep")
243 },
244 want: map[string]string{"a": "new", "c": "keep"},
245 },
246 {
247 name: "recovery after flush then more WAL",
248 ops: func(t *testing.T, db *DB) {
249 mustPut(t, db, "f", "flushed")
250 if err := db.Flush(); err != nil {
251 t.Fatal(err)
252 }
253 mustPut(t, db, "w", "wal-only")
254 mustPut(t, db, "f", "updated")
255 },
256 want: map[string]string{"f": "updated", "w": "wal-only"},
257 },
258 }
259
260 for _, tc := range tests {
261 t.Run(tc.name, func(t *testing.T) {
262 dir := t.TempDir()
263 db, err := Open(dir, testOpts())
264 if err != nil {
265 t.Fatal(err)
266 }
267 tc.ops(t, db)
268
269
270
271 db.mu.Lock()
272 if db.wal != nil {
273 _ = db.wal.f.Sync()
274 _ = db.wal.close()
275 }
276 for _, tab := range db.tables {
277 _ = tab.close()
278 }
279 db.closed = true
280 db.mu.Unlock()
281
282
283 db2, err := Open(dir, testOpts())
284 if err != nil {
285 t.Fatalf("reopen: %v", err)
286 }
287 defer db2.Close()
288
289 for k, v := range tc.want {
290 mustGet(t, db2, k, v, true)
291 }
292
293 if _, ok := tc.want["b"]; !ok && tc.name == "overwrite and delete in WAL" {
294 mustGet(t, db2, "b", "", false)
295 }
296 })
297 }
298}
299
300func TestWALPartialRecordIgnored(t *testing.T) {
301 dir := t.TempDir()
302 db, err := Open(dir, testOpts())
303 if err != nil {
304 t.Fatal(err)
305 }
306 mustPut(t, db, "ok", "yes")
307
308 db.mu.Lock()
309 walPath := db.wal.path
310 _ = db.wal.f.Sync()
311 _ = db.wal.close()
312 for _, tab := range db.tables {
313 _ = tab.close()
314 }
315 db.closed = true
316 db.mu.Unlock()
317
318 f, err := os.OpenFile(walPath, os.O_APPEND|os.O_WRONLY, 0o644)
319 if err != nil {
320 t.Fatal(err)
321 }
322
323 if _, err := f.Write([]byte{walOpPut, 0x10}); err != nil {
324 t.Fatal(err)
325 }
326 _ = f.Close()
327
328 db2, err := Open(dir, testOpts())
329 if err != nil {
330 t.Fatalf("reopen after partial WAL: %v", err)
331 }
332 defer db2.Close()
333 mustGet(t, db2, "ok", "yes", true)
334}
335
336func TestCompactionCorrectness(t *testing.T) {
337 tests := []struct {
338 name string
339 run func(t *testing.T, db *DB)
340 }{
341 {
342 name: "merge overlapping tables keeps newest",
343 run: func(t *testing.T, db *DB) {
344
345 mustPut(t, db, "a", "1")
346 mustPut(t, db, "b", "1")
347 if err := db.Flush(); err != nil {
348 t.Fatal(err)
349 }
350 mustPut(t, db, "a", "2")
351 mustPut(t, db, "c", "1")
352 if err := db.Flush(); err != nil {
353 t.Fatal(err)
354 }
355 mustPut(t, db, "b", "2")
356 if err := db.Flush(); err != nil {
357 t.Fatal(err)
358 }
359
360 _, nBefore := db.Stats()
361 if nBefore < 2 {
362 t.Fatalf("expected multiple tables, got %d", nBefore)
363 }
364 if err := db.Compact(); err != nil {
365 t.Fatal(err)
366 }
367 _, nAfter := db.Stats()
368 if nAfter >= nBefore {
369 t.Fatalf("compaction should reduce tables: before=%d after=%d", nBefore, nAfter)
370 }
371 mustGet(t, db, "a", "2", true)
372 mustGet(t, db, "b", "2", true)
373 mustGet(t, db, "c", "1", true)
374 },
375 },
376 {
377 name: "compaction drops deleted keys",
378 run: func(t *testing.T, db *DB) {
379 mustPut(t, db, "x", "1")
380 mustPut(t, db, "y", "1")
381 if err := db.Flush(); err != nil {
382 t.Fatal(err)
383 }
384 mustDelete(t, db, "x")
385 mustPut(t, db, "y", "2")
386 if err := db.Flush(); err != nil {
387 t.Fatal(err)
388 }
389 if err := db.Compact(); err != nil {
390 t.Fatal(err)
391 }
392 mustGet(t, db, "x", "", false)
393 mustGet(t, db, "y", "2", true)
394 },
395 },
396 {
397 name: "non-overlapping tables stay separate",
398 run: func(t *testing.T, db *DB) {
399
400 mustPut(t, db, "a", "1")
401 mustPut(t, db, "b", "1")
402 if err := db.Flush(); err != nil {
403 t.Fatal(err)
404 }
405 mustPut(t, db, "y", "1")
406 mustPut(t, db, "z", "1")
407 if err := db.Flush(); err != nil {
408 t.Fatal(err)
409 }
410 _, nBefore := db.Stats()
411 if err := db.Compact(); err != nil {
412 t.Fatal(err)
413 }
414 _, nAfter := db.Stats()
415 if nAfter != nBefore {
416 t.Fatalf("non-overlapping tables should not merge: before=%d after=%d", nBefore, nAfter)
417 }
418 mustGet(t, db, "a", "1", true)
419 mustGet(t, db, "z", "1", true)
420 },
421 },
422 {
423 name: "data correct after compact and reopen",
424 run: func(t *testing.T, db *DB) {
425 for i := 0; i < 20; i++ {
426 mustPut(t, db, fmt.Sprintf("k%02d", i), fmt.Sprintf("v%d", i))
427 if i%5 == 4 {
428 if err := db.Flush(); err != nil {
429 t.Fatal(err)
430 }
431 }
432 }
433 mustDelete(t, db, "k05")
434 mustPut(t, db, "k10", "updated")
435 if err := db.Flush(); err != nil {
436 t.Fatal(err)
437 }
438 if err := db.Compact(); err != nil {
439 t.Fatal(err)
440 }
441
442 },
443 },
444 }
445
446 for _, tc := range tests {
447 t.Run(tc.name, func(t *testing.T) {
448 db, dir := openTestDB(t, testOpts())
449 tc.run(t, db)
450 if tc.name == "data correct after compact and reopen" {
451 _ = db.Close()
452 db2, err := Open(dir, testOpts())
453 if err != nil {
454 t.Fatal(err)
455 }
456 defer db2.Close()
457 mustGet(t, db2, "k00", "v0", true)
458 mustGet(t, db2, "k05", "", false)
459 mustGet(t, db2, "k10", "updated", true)
460 mustGet(t, db2, "k19", "v19", true)
461 }
462 })
463 }
464}
465
466func TestRangeIterator(t *testing.T) {
467 db, _ := openTestDB(t, testOpts())
468
469 for i := 0; i < 30; i++ {
470 mustPut(t, db, fmt.Sprintf("k%02d", i), fmt.Sprintf("v%02d", i))
471 if i == 10 || i == 20 {
472 if err := db.Flush(); err != nil {
473 t.Fatal(err)
474 }
475 }
476 }
477 mustDelete(t, db, "k05")
478 mustPut(t, db, "k15", "new")
479
480 it := db.NewIterator([]byte("k03"), []byte("k18"))
481 if it.Err() != nil {
482 t.Fatal(it.Err())
483 }
484 var got []string
485 for ; it.Valid(); it.Next() {
486 got = append(got, fmt.Sprintf("%s=%s", it.Key(), it.Value()))
487 }
488 if it.Err() != nil {
489 t.Fatal(it.Err())
490 }
491
492
493 var want []string
494 for i := 3; i < 18; i++ {
495 if i == 5 {
496 continue
497 }
498 v := fmt.Sprintf("v%02d", i)
499 if i == 15 {
500 v = "new"
501 }
502 want = append(want, fmt.Sprintf("k%02d=%s", i, v))
503 }
504 if len(got) != len(want) {
505 t.Fatalf("range len got %d want %d\ngot=%v\nwant=%v", len(got), len(want), got, want)
506 }
507 for i := range want {
508 if got[i] != want[i] {
509 t.Fatalf("range[%d]=%q want %q", i, got[i], want[i])
510 }
511 }
512}
513
514func TestFlushCreatesSSTable(t *testing.T) {
515 db, dir := openTestDB(t, testOpts())
516 mustPut(t, db, "a", "1")
517 if err := db.Flush(); err != nil {
518 t.Fatal(err)
519 }
520 mem, n := db.Stats()
521 if mem != 0 {
522 t.Fatalf("memtable should be empty after flush, got %d", mem)
523 }
524 if n != 1 {
525 t.Fatalf("expected 1 table, got %d", n)
526 }
527
528 matches, _ := filepath.Glob(filepath.Join(dir, "*.sst"))
529 if len(matches) != 1 {
530 t.Fatalf("expected 1 sst file, got %v", matches)
531 }
532 mustGet(t, db, "a", "1", true)
533}
534
535func TestEmptyKeyRejected(t *testing.T) {
536 db, _ := openTestDB(t, testOpts())
537 if err := db.Put(nil, []byte("v")); err == nil {
538 t.Fatal("expected error for empty key Put")
539 }
540 if err := db.Delete([]byte{}); err == nil {
541 t.Fatal("expected error for empty key Delete")
542 }
543}
544
545func TestRandomizedAgainstOracle(t *testing.T) {
546 const N = 10000
547 rng := rand.New(rand.NewSource(42))
548
549 db, dir := openTestDB(t, Options{
550 MemtableBytes: 2048,
551 SparseIndexInterval: 8,
552 })
553
554 oracle := map[string]string{}
555 keys := make([]string, 0, 500)
556
557 randKey := func() string {
558 if len(keys) > 0 && rng.Intn(3) != 0 {
559 return keys[rng.Intn(len(keys))]
560 }
561 k := fmt.Sprintf("k%05d", rng.Intn(800))
562
563 found := false
564 for _, existing := range keys {
565 if existing == k {
566 found = true
567 break
568 }
569 }
570 if !found {
571 keys = append(keys, k)
572 }
573 return k
574 }
575
576 for i := 0; i < N; i++ {
577 switch rng.Intn(10) {
578 case 0, 1, 2, 3, 4:
579 k := randKey()
580 v := fmt.Sprintf("v%d-%d", i, rng.Intn(1000))
581 if err := db.Put([]byte(k), []byte(v)); err != nil {
582 t.Fatalf("op %d Put: %v", i, err)
583 }
584 oracle[k] = v
585 case 5, 6:
586 k := randKey()
587 if err := db.Delete([]byte(k)); err != nil {
588 t.Fatalf("op %d Delete: %v", i, err)
589 }
590 delete(oracle, k)
591 case 7:
592 k := randKey()
593 want, wantOK := oracle[k]
594 got, ok, err := db.Get([]byte(k))
595 if err != nil {
596 t.Fatalf("op %d Get: %v", i, err)
597 }
598 if ok != wantOK || (ok && string(got) != want) {
599 t.Fatalf("op %d Get(%q): got (%q,%v) want (%q,%v)", i, k, got, ok, want, wantOK)
600 }
601 case 8:
602 if err := db.Flush(); err != nil {
603 t.Fatalf("op %d Flush: %v", i, err)
604 }
605 default:
606 if err := db.Compact(); err != nil {
607 t.Fatalf("op %d Compact: %v", i, err)
608 }
609 }
610
611
612 if (i+1)%2000 == 0 {
613 checkOracle(t, db, oracle)
614 }
615 }
616
617
618 checkOracle(t, db, oracle)
619
620
621 _ = db.Close()
622 db2, err := Open(dir, Options{MemtableBytes: 2048, SparseIndexInterval: 8})
623 if err != nil {
624 t.Fatal(err)
625 }
626 defer db2.Close()
627 checkOracle(t, db2, oracle)
628
629
630 it := db2.NewIterator(nil, nil)
631 var scanned []string
632 for ; it.Valid(); it.Next() {
633 scanned = append(scanned, string(it.Key())+"="+string(it.Value()))
634 }
635 if it.Err() != nil {
636 t.Fatal(it.Err())
637 }
638 var expected []string
639 for k, v := range oracle {
640 expected = append(expected, k+"="+v)
641 }
642 sort.Strings(expected)
643 sort.Strings(scanned)
644 if len(scanned) != len(expected) {
645 t.Fatalf("range scan len %d want %d", len(scanned), len(expected))
646 }
647 for i := range expected {
648 if scanned[i] != expected[i] {
649 t.Fatalf("range[%d]=%q want %q", i, scanned[i], expected[i])
650 }
651 }
652}
653
654func checkOracle(t *testing.T, db *DB, oracle map[string]string) {
655 t.Helper()
656 for k, want := range oracle {
657 got, ok, err := db.Get([]byte(k))
658 if err != nil {
659 t.Fatalf("Get(%q): %v", k, err)
660 }
661 if !ok || string(got) != want {
662 t.Fatalf("Get(%q): got (%q,%v) want (%q,true)", k, got, ok, want)
663 }
664 }
665
666 for i := 0; i < 50; i++ {
667 k := fmt.Sprintf("missing-%d", i)
668 if _, exists := oracle[k]; exists {
669 continue
670 }
671 _, ok, err := db.Get([]byte(k))
672 if err != nil {
673 t.Fatal(err)
674 }
675 if ok {
676 t.Fatalf("Get(%q) should miss", k)
677 }
678 }
679}
680
681func TestBloomAndSparseIndex(t *testing.T) {
682
683 db, _ := openTestDB(t, Options{MemtableBytes: 1 << 20, SparseIndexInterval: 8})
684 const n = 200
685 for i := 0; i < n; i++ {
686 mustPut(t, db, fmt.Sprintf("key-%04d", i), fmt.Sprintf("val-%d", i))
687 }
688 if err := db.Flush(); err != nil {
689 t.Fatal(err)
690 }
691 for i := 0; i < n; i++ {
692 mustGet(t, db, fmt.Sprintf("key-%04d", i), fmt.Sprintf("val-%d", i), true)
693 }
694
695 for i := 0; i < 20; i++ {
696 mustGet(t, db, fmt.Sprintf("nope-%d", i), "", false)
697 }
698}
699
700func TestMemtableSorted(t *testing.T) {
701 m := newMemtable()
702 m.put([]byte("c"), []byte("3"))
703 m.put([]byte("a"), []byte("1"))
704 m.put([]byte("b"), []byte("2"))
705 m.delete([]byte("b"))
706 snap := m.snapshot()
707 if len(snap) != 3 {
708 t.Fatalf("len=%d", len(snap))
709 }
710 if !bytes.Equal(snap[0].key, []byte("a")) || !bytes.Equal(snap[2].key, []byte("c")) {
711 t.Fatalf("not sorted: %q %q %q", snap[0].key, snap[1].key, snap[2].key)
712 }
713 if !snap[1].deleted {
714 t.Fatal("b should be tombstone")
715 }
716}
717
Discussion
No comments yet. Start the discussion. Recorded by @patrick-toulme.