package mdb import ( "errors" "fmt" "os" "path/filepath" "reflect" "testing" ) func TestMapIndex(t *testing.T) { type Item struct { ID uint64 Name string ExtID string } checkIdxOne := func(idx *MapIndex[string, Item], expectedList ...Item) error { expected := make(map[string]Item, len(expectedList)) for _, i := range expectedList { expected[i.ExtID] = i } if len(expected) != len(idx.m) { return fmt.Errorf("Expected %d items, but got %d.", len(expected), len(idx.m)) } for _, e := range expected { i, ok := idx.Get(e.ExtID) if !ok { return fmt.Errorf("Missing item: %v", e) } if !reflect.DeepEqual(i, e) { return fmt.Errorf("Items not equal: %v != %v", i, e) } } return nil } checkIdx := func(idx *MapIndex[string, Item], expectedList ...Item) error { idx.c.db.waitForWAL() if err := checkIdxOne(idx, expectedList...); err != nil { return fmt.Errorf("%w: original", err) } // Reload the database, collection, and index and re-test. db := NewPrimary(idx.c.db.root) c := NewCollection(db, "collection", func(i *Item) uint64 { return i.ID }) idx = NewMapIndex(c, "ExtID", func(i *Item) string { return i.ExtID }, func(i *Item) bool { return i.ExtID != "" }) db.Start() return checkIdxOne(idx, expectedList...) } run := func(name string, inner func(t *testing.T, c *Collection[Item], idx *MapIndex[string, Item])) { t.Run(name, func(t *testing.T) { root := filepath.Join(os.TempDir(), randString()) defer os.RemoveAll(root) db := NewPrimary(root) defer db.Close() c := NewCollection(db, "collection", func(i *Item) uint64 { return i.ID }) idx := NewMapIndex(c, "ExtID", func(i *Item) string { return i.ExtID }, func(i *Item) bool { return i.ExtID != "" }) db.Start() inner(t, c, idx) }) } run("insert item not in index", func(t *testing.T, c *Collection[Item], idx *MapIndex[string, Item]) { item := Item{4, "4", ""} c.Insert(item) if err := checkIdx(idx); err != nil { t.Fatal(err) } }) run("insert item in index", func(t *testing.T, c *Collection[Item], idx *MapIndex[string, Item]) { item1 := Item{4, "4", ""} item2 := Item{5, "5", "abcd"} c.Insert(item1) c.Insert(item2) if err := checkIdx(idx, item2); err != nil { t.Fatal(err) } }) run("insert several items", func(t *testing.T, c *Collection[Item], idx *MapIndex[string, Item]) { item1 := Item{4, "4", ""} item2 := Item{5, "5", "abcd"} item3 := Item{6, "6", ""} item4 := Item{7, "7", "xyz"} item5 := Item{8, "8", ""} item6 := Item{9, "9", "mmm"} c.Insert(item1) c.Insert(item2) c.Insert(item3) c.Insert(item4) c.Insert(item5) c.Insert(item6) if err := checkIdx(idx, item2, item4, item6); err != nil { t.Fatal(err) } }) run("insert with conflict", func(t *testing.T, c *Collection[Item], idx *MapIndex[string, Item]) { item1 := Item{1, "1", "one"} item2 := Item{2, "2", "one"} c.Insert(item1) if _, err := c.Insert(item2); !errors.Is(err, ErrDuplicate) { t.Fatal(err) } }) run("update into index", func(t *testing.T, c *Collection[Item], idx *MapIndex[string, Item]) { item1 := Item{1, "1", ""} c.Insert(item1) if err := checkIdx(idx); err != nil { t.Fatal(err) } err := c.Update(1, func(i Item) (Item, error) { i.ExtID = "xx" return i, nil }) if err != nil { t.Fatal(err) } item1.ExtID = "xx" if err := checkIdx(idx, item1); err != nil { t.Fatal(err) } }) run("update out of index", func(t *testing.T, c *Collection[Item], idx *MapIndex[string, Item]) { c.Insert(Item{1, "1", ""}) c.Insert(Item{2, "2", "two"}) // In index. c.Insert(Item{3, "3", ""}) err := c.Update(1, func(in Item) (Item, error) { in.Name = "ONE" return in, nil }) if err != nil { t.Fatal(err) } if err := checkIdx(idx, Item{2, "2", "two"}); err != nil { t.Fatal(err) } }) run("update out of index conflict", func(t *testing.T, c *Collection[Item], idx *MapIndex[string, Item]) { c.Insert(Item{1, "1", ""}) c.Insert(Item{2, "2", "two"}) // In index. c.Insert(Item{3, "3", ""}) err := c.Update(1, func(in Item) (Item, error) { in.ExtID = "two" return in, nil }) if !errors.Is(err, ErrDuplicate) { t.Fatal(err) } }) run("update within index", func(t *testing.T, c *Collection[Item], idx *MapIndex[string, Item]) { c.Insert(Item{1, "1", "one"}) c.Insert(Item{2, "2", "two"}) // In index. c.Insert(Item{3, "3", ""}) err := c.Update(2, func(in Item) (Item, error) { in.ExtID = "TWO" return in, nil }) if err != nil { t.Fatal(err) } if err := checkIdx(idx, Item{1, "1", "one"}, Item{2, "2", "TWO"}); err != nil { t.Fatal(err) } }) run("update using index", func(t *testing.T, c *Collection[Item], idx *MapIndex[string, Item]) { c.Insert(Item{1, "1", "one"}) c.Insert(Item{2, "2", "two"}) // In index. c.Insert(Item{3, "3", ""}) err := idx.Update("one", func(in Item) (Item, error) { in.Name = "_1_" return in, nil }) if err != nil { t.Fatal(err) } if err := checkIdx(idx, Item{1, "_1_", "one"}, Item{2, "2", "two"}); err != nil { t.Fatal(err) } }) run("update using index not found", func(t *testing.T, c *Collection[Item], idx *MapIndex[string, Item]) { c.Insert(Item{1, "1", "one"}) c.Insert(Item{3, "3", ""}) err := idx.Update("onex", func(in Item) (Item, error) { in.Name = "_1_" return in, nil }) if !errors.Is(err, ErrNotFound) { t.Fatal(err) } }) run("update using index caller error", func(t *testing.T, c *Collection[Item], idx *MapIndex[string, Item]) { c.Insert(Item{1, "1", "one"}) c.Insert(Item{3, "3", ""}) myErr := errors.New("Mine") err := idx.Update("one", func(in Item) (Item, error) { in.Name = "_1_" return in, myErr }) if !errors.Is(err, myErr) { t.Fatal(err) } }) run("update using index mismatched IDs", func(t *testing.T, c *Collection[Item], idx *MapIndex[string, Item]) { c.Insert(Item{1, "1", "one"}) err := idx.Update("one", func(in Item) (Item, error) { in.ExtID = "onex" return in, nil }) if !errors.Is(err, ErrMismatchedIDs) { t.Fatal(err) } }) run("delete out of index", func(t *testing.T, c *Collection[Item], idx *MapIndex[string, Item]) { c.Insert(Item{1, "1", "one"}) c.Insert(Item{2, "2", "two"}) // In index. c.Insert(Item{3, "3", ""}) c.Delete(3) if err := checkIdx(idx, Item{1, "1", "one"}, Item{2, "2", "two"}); err != nil { t.Fatal(err) } }) run("delete from index", func(t *testing.T, c *Collection[Item], idx *MapIndex[string, Item]) { c.Insert(Item{1, "1", "one"}) c.Insert(Item{2, "2", "two"}) // In index. c.Insert(Item{3, "3", ""}) c.Delete(2) if err := checkIdx(idx, Item{1, "1", "one"}); err != nil { t.Fatal(err) } }) run("delete using index", func(t *testing.T, c *Collection[Item], idx *MapIndex[string, Item]) { c.Insert(Item{1, "1", "one"}) c.Insert(Item{2, "2", "two"}) // In index. c.Insert(Item{3, "3", ""}) idx.Delete("two") if err := checkIdx(idx, Item{1, "1", "one"}); err != nil { t.Fatal(err) } }) run("delete using index not found", func(t *testing.T, c *Collection[Item], idx *MapIndex[string, Item]) { c.Insert(Item{1, "1", "one"}) c.Insert(Item{2, "2", "two"}) // In index. c.Insert(Item{3, "3", ""}) idx.Delete("onex") if err := checkIdx(idx, Item{1, "1", "one"}, Item{2, "2", "two"}); err != nil { t.Fatal(err) } }) run("check name", func(t *testing.T, c *Collection[Item], idx *MapIndex[string, Item]) { if idx.name() != "ExtID" { t.Fatal(idx.name()) } }) } func TestMapIndexLoadError(t *testing.T) { type Item struct { ID uint64 Name string ExtID string } root := filepath.Join(os.TempDir(), randString()) defer os.RemoveAll(root) db := NewPrimary(root) c := NewCollection(db, "collection", func(i *Item) uint64 { return i.ID }) db.Start() defer db.Close() c.Insert(Item{1, "one", "x"}) c.Insert(Item{2, "two", "x"}) c.Insert(Item{3, "three", "y"}) c.Insert(Item{4, "x", ""}) idx := NewMapIndex(c, "ExtID", func(i *Item) string { return i.ExtID }, func(i *Item) bool { return i.ExtID != "" }) err := idx.load(c.items.m) if !errors.Is(err, ErrDuplicate) { t.Fatal(err) } }