From f4d72247e464c68485c260a8d7788835d04c17c5 Mon Sep 17 00:00:00 2001 From: jdl Date: Wed, 27 Jul 2022 09:56:01 +0200 Subject: [PATCH] More testing. --- README.md | 9 +- btreeindex.go | 2 +- btreeindex_ex_test.go | 98 +++++++++- btreeindex_test.go | 429 +++++++++--------------------------------- mapindex.go | 4 - mapindex_test.go | 357 ++++++++++++++++++++++++++++++++--- 6 files changed, 524 insertions(+), 375 deletions(-) diff --git a/README.md b/README.md index 174eedb..f269d6d 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,14 @@ An in-process, in-memory database for Go. ## TO DO * mapindex_test.go - * TestFullMapIndex - * TestPartialMapIndex + * ~~TestFullMapIndex~~ * btreeindex_test.go - * TestFullBTreeIndex * TestPartialBTreeIndex -* btreeiterator_test.go +* btreeiterator_test.go (?) * collection * database * WAL shipping * WAL shipping with network disconnects + +* BTreeIndex: + * Should insert panic if item is replaced? diff --git a/btreeindex.go b/btreeindex.go index a33fd7d..722bebd 100644 --- a/btreeindex.go +++ b/btreeindex.go @@ -29,7 +29,7 @@ func NewBTreeIndex[T any]( include: include, } - btree := btree.NewG(64, less) + btree := btree.NewG(32, less) t.bt.Store(btree) c.indices = append(c.indices, t) diff --git a/btreeindex_ex_test.go b/btreeindex_ex_test.go index 0806d38..02eb8b0 100644 --- a/btreeindex_ex_test.go +++ b/btreeindex_ex_test.go @@ -35,17 +35,103 @@ func (bt *BTreeIndex[T]) EqualsList(data []*T) error { return fmt.Errorf("Expected %d items, but found %d.", bt.Len(), len(data)) } - it1 := bt.Ascend() - defer it1.Close() + if len(data) == 0 { + return nil + } + // Ascend fully. + it := bt.Ascend() for _, v1 := range data { - it1.Next() - v2 := it1.Value() + it.Next() + v2 := it.Value() - if !reflect.DeepEqual(v1, v2) { - return fmt.Errorf("Value mismatch: %v != %v", v1, v2) + if !reflect.DeepEqual(*v1, v2) { + return fmt.Errorf("Value mismatch: %v != %v", *v1, v2) } } + if it.Next() { + return fmt.Errorf("Next returned true after full ascend.") + } + it.Close() + + // Descend fully. + it = bt.Descend() + dataList := data + for len(dataList) > 0 { + v1 := dataList[len(dataList)-1] + dataList = dataList[:len(dataList)-1] + it.Next() + v2 := it.Value() + + if !reflect.DeepEqual(*v1, v2) { + return fmt.Errorf("Value mismatch: %v != %v", *v1, v2) + } + } + if it.Next() { + return fmt.Errorf("Next returned true after full descend.") + } + it.Close() + + // AscendAfter + dataList = data + for len(dataList) > 1 { + dataList = dataList[1:] + it = bt.AscendAfter(*dataList[0]) + + for _, v1 := range dataList { + it.Next() + v2 := it.Value() + if !reflect.DeepEqual(*v1, v2) { + return fmt.Errorf("Value mismatch: %v != %v", *v1, v2) + } + } + if it.Next() { + return fmt.Errorf("Next returned true after partial ascend.") + } + it.Close() + } + + // DescendAfter + dataList = data + for len(dataList) > 1 { + dataList = dataList[:len(dataList)-1] + it = bt.DescendAfter(*dataList[len(dataList)-1]) + + for i := len(dataList) - 1; i >= 0; i-- { + v1 := dataList[i] + it.Next() + v2 := it.Value() + if !reflect.DeepEqual(*v1, v2) { + return fmt.Errorf("Value mismatch: %v != %v", *v1, v2) + } + } + if it.Next() { + return fmt.Errorf("Next returned true after partial descend: %#v", it.Value()) + } + it.Close() + } + + // Using Get. + for _, v1 := range data { + v2, ok := bt.Get(*v1) + if !ok || !reflect.DeepEqual(*v1, v2) { + return fmt.Errorf("Value mismatch: %v != %v", *v1, v2) + } + } + + // Min. + v1 := data[0] + v2, ok := bt.Min() + if !ok || !reflect.DeepEqual(*v1, v2) { + return fmt.Errorf("Value mismatch: %v != %v", *v1, v2) + } + + // Max. + v1 = data[len(data)-1] + v2, ok = bt.Max() + if !ok || !reflect.DeepEqual(*v1, v2) { + return fmt.Errorf("Value mismatch: %v != %v", *v1, v2) + } return nil } diff --git a/btreeindex_test.go b/btreeindex_test.go index ac23bbf..8ad7da7 100644 --- a/btreeindex_test.go +++ b/btreeindex_test.go @@ -1,376 +1,133 @@ package mdb -/* -func TestBTreeIndex(t *testing.T) { - type Item struct { - ID uint64 - Name string - } +import ( + "reflect" + "testing" +) - checkIndexOne := func(idx *BTreeIndex[Item], expected ...Item) error { - if idx.Len() != len(expected) { - return fmt.Errorf("Expected %d items but found %d.", len(expected), idx.Len()) - } +func TestFullBTreeIndex(t *testing.T) { - if len(expected) == 0 { - return nil - } + // Test against the email index. + run := func(name string, inner func(t *testing.T, db *DB) []*User) { + testWithDB(t, name, func(t *testing.T, db *DB) { + expected := inner(t, db) - for _, item := range expected { - item2, ok := idx.Get(item) - if !ok { - return fmt.Errorf("Missing expected item: %v", item) + if err := db.Users.emailBTree.EqualsList(expected); err != nil { + t.Fatal(err) } - if !reflect.DeepEqual(item, item2) { - return fmt.Errorf("Items not equal: %v != %v", item2, item) + + db.Close() + db = OpenDB(db.root, true) + + if err := db.Users.emailBTree.EqualsList(expected); err != nil { + t.Fatal(err) } - } - - item, ok := idx.Min() - if !ok { - return fmt.Errorf("Min item not found, expected: %v", expected[0]) - } - if !reflect.DeepEqual(item, expected[0]) { - return fmt.Errorf("Min items not equal: %v != %v", item, expected[0]) - } - - item, ok = idx.Max() - i := len(expected) - 1 - if !ok { - return fmt.Errorf("Max item not found, expected: %v", expected[i]) - } - if !reflect.DeepEqual(item, expected[i]) { - return fmt.Errorf("Max items not equal: %v != %v", item, expected[i]) - } - - i = 0 - - iter := idx.Ascend() - defer iter.Close() - for iter.Next() { - if !reflect.DeepEqual(iter.Value(), expected[i]) { - return fmt.Errorf("Items not equal (%d): %v != %v", i, iter.Value(), expected[i]) - } - i++ - } - - i = len(expected) - 1 - iter = idx.Descend() - defer iter.Close() - for iter.Next() { - if !reflect.DeepEqual(iter.Value(), expected[i]) { - return fmt.Errorf("Items not equal (%d): %v != %v", i, iter.Value(), expected[i]) - } - i-- - } - - i = 1 - iter = idx.AscendAfter(expected[1]) - defer iter.Close() - for iter.Next() { - if !reflect.DeepEqual(iter.Value(), expected[i]) { - return fmt.Errorf("Items not equal (%d): %v != %v", i, iter.Value(), expected[i]) - } - i++ - } - - i = len(expected) - 2 - iter = idx.DescendAfter(expected[len(expected)-2]) - defer iter.Close() - for iter.Next() { - if !reflect.DeepEqual(iter.Value(), expected[i]) { - return fmt.Errorf("Items not equal (%d): %v != %v", i, iter.Value(), expected[i]) - } - i-- - } - - return nil - } - - checkIndex := func(idx *BTreeIndex[Item], expected ...Item) error { - idx.c.db.waitForWAL() - - if err := checkIndexOne(idx, expected...); err != nil { - return fmt.Errorf("%w: original", err) - } - - db := NewPrimary(idx.c.db.root) - defer db.Close() - c := NewCollection(db, "collection", func(i *Item) uint64 { return i.ID }) - idx = NewBTreeIndex(c, - func(i, j *Item) bool { return i.Name < j.Name }, - func(i *Item) bool { return i.Name != "" }) - db.Start() - - return checkIndexOne(idx, expected...) - } - - run := func(name string, inner func(t *testing.T, c *Collection[Item], idx *BTreeIndex[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 := NewBTreeIndex(c, - func(i, j *Item) bool { return i.Name < j.Name }, - func(i *Item) bool { return i.Name != "" }) - - db.Start() - - inner(t, c, idx) }) } - run("no items", func(t *testing.T, c *Collection[Item], idx *BTreeIndex[Item]) { - if err := checkIndex(idx); err != nil { - t.Fatal(err) + run("insert", func(t *testing.T, db *DB) (users []*User) { + users = append(users, + &User{ID: db.Users.c.NextID(), Email: "a@b.com", Name: "aaa"}, + &User{ID: db.Users.c.NextID(), Email: "c@d.com", Name: "ccc"}) + + for _, u := range users { + u2, err := db.Users.c.Insert(*u) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(u2, *u) { + t.Fatal(u2, *u) + } } + + return users }) - run("insert some", func(t *testing.T, c *Collection[Item], idx *BTreeIndex[Item]) { - item1 := Item{1, "one"} - item2 := Item{2, ""} - item3 := Item{3, "three"} - item4 := Item{4, ""} - item5 := Item{5, "five"} + // Update - c.Insert(item1) - c.Insert(item2) - c.Insert(item3) - c.Insert(item4) - c.Insert(item5) + run("update", func(t *testing.T, db *DB) (users []*User) { + users = append(users, + &User{ID: db.Users.c.NextID(), Email: "a@b.com", Name: "aaa"}, + &User{ID: db.Users.c.NextID(), Email: "e@f.com", Name: "eee"}, + &User{ID: db.Users.c.NextID(), Email: "c@d.com", Name: "ccc"}) - if err := checkIndex(idx, item5, item1, item3); err != nil { - t.Fatal(err) - } - }) - - run("partial iteration", func(t *testing.T, c *Collection[Item], idx *BTreeIndex[Item]) { - item1 := Item{1, "one"} - item2 := Item{2, ""} - item3 := Item{3, "three"} - item4 := Item{4, ""} - item5 := Item{5, "five"} - - c.Insert(item1) - c.Insert(item2) - c.Insert(item3) - c.Insert(item4) - c.Insert(item5) - - iter := idx.Ascend() - defer iter.Close() - - if !iter.Next() { - t.Fatal("Expected", item5) + for _, u := range users { + if _, err := db.Users.c.Insert(*u); err != nil { + t.Fatal(err) + } } - if !reflect.DeepEqual(iter.Value(), item5) { - t.Fatal(iter.Value(), item5) - } - }) - - run("get", func(t *testing.T, c *Collection[Item], idx *BTreeIndex[Item]) { - item1 := Item{1, "one"} - item2 := Item{2, ""} - c.Insert(item1) - c.Insert(item2) - - item, ok := idx.Get(Item{0, "one"}) - if !ok || !reflect.DeepEqual(item, item1) { - t.Fatal(ok, item, item1) - } - }) - - run("get not found", func(t *testing.T, c *Collection[Item], idx *BTreeIndex[Item]) { - item1 := Item{1, "one"} - item2 := Item{2, ""} - c.Insert(item1) - c.Insert(item2) - - if item, ok := idx.Get(Item{0, "three"}); ok { - t.Fatal(item) - } - }) - - run("min max on empty", func(t *testing.T, c *Collection[Item], idx *BTreeIndex[Item]) { - item2 := Item{2, ""} - c.Insert(item2) - - if item, ok := idx.Min(); ok { - t.Fatal(item) - } - if item, ok := idx.Max(); ok { - t.Fatal(item) - } - }) - - run("min max with one item", func(t *testing.T, c *Collection[Item], idx *BTreeIndex[Item]) { - item1 := Item{1, "one"} - item2 := Item{2, ""} - - c.Insert(item1) - c.Insert(item2) - - i1, ok := idx.Min() - if !ok { - t.Fatal(ok) - } - i2, ok := idx.Max() - if !ok { - t.Fatal(ok) - } - - if !reflect.DeepEqual(i1, i2) { - t.Fatal(i1, i2) - } - }) - - run("update outside of index", func(t *testing.T, c *Collection[Item], idx *BTreeIndex[Item]) { - item1 := Item{1, "one"} - item2 := Item{2, ""} - item3 := Item{3, "three"} - item4 := Item{4, ""} - item5 := Item{5, "five"} - - c.Insert(item1) - c.Insert(item2) - c.Insert(item3) - c.Insert(item4) - c.Insert(item5) - - c.Update(2, func(in Item) (Item, error) { - return in, nil - }) - - if err := checkIndex(idx, item5, item1, item3); err != nil { - t.Fatal(err) - } - }) - - run("update into index", func(t *testing.T, c *Collection[Item], idx *BTreeIndex[Item]) { - item1 := Item{1, "one"} - item2 := Item{2, ""} - item3 := Item{3, "three"} - item4 := Item{4, ""} - item5 := Item{5, "five"} - - c.Insert(item1) - c.Insert(item2) - c.Insert(item3) - c.Insert(item4) - c.Insert(item5) - - err := c.Update(2, func(in Item) (Item, error) { - in.Name = "two" - return in, nil + err := db.Users.c.Update(users[2].ID, func(u User) (User, error) { + u.Email = "g@h.com" + return u, nil }) if err != nil { t.Fatal(err) } + users[2].Email = "g@h.com" - item2.Name = "two" - - if err := checkIndex(idx, item5, item1, item3, item2); err != nil { - t.Fatal(err) - } + return users }) - run("update out of index", func(t *testing.T, c *Collection[Item], idx *BTreeIndex[Item]) { - item1 := Item{1, "one"} - item2 := Item{2, ""} - item3 := Item{3, "three"} - item4 := Item{4, ""} - item5 := Item{5, "five"} + run("delete", func(t *testing.T, db *DB) (users []*User) { + users = append(users, + &User{ID: db.Users.c.NextID(), Email: "a@b.com", Name: "aaa"}, + &User{ID: db.Users.c.NextID(), Email: "c@d.com", Name: "ccc"}, + &User{ID: db.Users.c.NextID(), Email: "e@f.com", Name: "eee"}) - c.Insert(item1) - c.Insert(item2) - c.Insert(item3) - c.Insert(item4) - c.Insert(item5) - - err := c.Update(1, func(in Item) (Item, error) { - in.Name = "" - return in, nil - }) - if err != nil { - t.Fatal(err) + for _, u := range users { + if _, err := db.Users.c.Insert(*u); err != nil { + t.Fatal(err) + } } - if err := checkIndex(idx, item5, item3); err != nil { - t.Fatal(err) - } + db.Users.c.Delete(users[0].ID) + users = users[1:] + + return users }) - run("update within index", func(t *testing.T, c *Collection[Item], idx *BTreeIndex[Item]) { - item1 := Item{1, "one"} - item2 := Item{2, ""} - item3 := Item{3, "three"} - item4 := Item{4, ""} - item5 := Item{5, "five"} + run("get not found", func(t *testing.T, db *DB) (users []*User) { + users = append(users, + &User{ID: db.Users.c.NextID(), Email: "a@b.com", Name: "aaa"}, + &User{ID: db.Users.c.NextID(), Email: "c@d.com", Name: "ccc"}, + &User{ID: db.Users.c.NextID(), Email: "e@f.com", Name: "eee"}) - c.Insert(item1) - c.Insert(item2) - c.Insert(item3) - c.Insert(item4) - c.Insert(item5) - - err := c.Update(1, func(in Item) (Item, error) { - in.Name = "xone" - return in, nil - }) - if err != nil { - t.Fatal(err) + for _, u := range users { + if _, err := db.Users.c.Insert(*u); err != nil { + t.Fatal(err) + } } - item1.Name = "xone" - - if err := checkIndex(idx, item5, item3, item1); err != nil { - t.Fatal(err) + if u, ok := db.Users.emailBTree.Get(User{Email: "g@h.com"}); ok { + t.Fatal(u, ok) } + + return users }) - run("delete outside index", func(t *testing.T, c *Collection[Item], idx *BTreeIndex[Item]) { - item1 := Item{1, "one"} - item2 := Item{2, ""} - item3 := Item{3, "three"} - item4 := Item{4, ""} - item5 := Item{5, "five"} + run("min/max empty", func(t *testing.T, db *DB) (users []*User) { - c.Insert(item1) - c.Insert(item2) - c.Insert(item3) - c.Insert(item4) - c.Insert(item5) - - c.Delete(item2.ID) - - if err := checkIndex(idx, item5, item1, item3); err != nil { - t.Fatal(err) + if u, ok := db.Users.emailBTree.Min(); ok { + t.Fatal(u, ok) } - }) - - run("delete within index", func(t *testing.T, c *Collection[Item], idx *BTreeIndex[Item]) { - item1 := Item{1, "one"} - item2 := Item{2, ""} - item3 := Item{3, "three"} - item4 := Item{4, ""} - item5 := Item{5, "five"} - - c.Insert(item1) - c.Insert(item2) - c.Insert(item3) - c.Insert(item4) - c.Insert(item5) - - c.Delete(item1.ID) - - if err := checkIndex(idx, item5, item3); err != nil { - t.Fatal(err) + if u, ok := db.Users.emailBTree.Max(); ok { + t.Fatal(u, ok) } + + return users }) } -*/ + +func TestPartialBTreeIndex(t *testing.T) { + // empty + // insert into + // insert outside + // upate out to in + // update out to out + // update in to in + // update in to out + // delete outside + // delete in + // load w/duplicate +} diff --git a/mapindex.go b/mapindex.go index 9fe01e4..90d849e 100644 --- a/mapindex.go +++ b/mapindex.go @@ -72,10 +72,6 @@ func (m *MapIndex[K, T]) Update(k K, update func(T) (T, error)) error { return item, err } - if m.getKey(&new) != k { - return item, ErrMismatchedIDs - } - return new, nil } diff --git a/mapindex_test.go b/mapindex_test.go index 0d25d63..5210059 100644 --- a/mapindex_test.go +++ b/mapindex_test.go @@ -51,8 +51,6 @@ func TestFullMapIndex(t *testing.T) { return users }) - // TODO: insert duplicate - run("delete", func(t *testing.T, db *DB) map[string]*User { users := map[string]*User{} @@ -171,7 +169,7 @@ func TestFullMapIndex(t *testing.T) { return users }) - run("update change key error", func(t *testing.T, db *DB) map[string]*User { + run("update change id error", func(t *testing.T, db *DB) map[string]*User { users := map[string]*User{} for i := uint64(1); i < 10; i++ { @@ -194,7 +192,7 @@ func TestFullMapIndex(t *testing.T) { } err := db.Users.emailMap.Update(email, func(u User) (User, error) { - u.Email = "test@x.com" + u.ID++ return u, nil }) if err != ErrMismatchedIDs { @@ -304,13 +302,7 @@ func TestFullMapIndex(t *testing.T) { run("insert conflict", func(t *testing.T, db *DB) map[string]*User { users := map[string]*User{} - - user := &User{ - ID: db.Users.c.NextID(), - Email: "a@b.com", - Name: "a", - ExtID: "", - } + user := &User{ID: db.Users.c.NextID(), Email: "a@b.com", Name: "a", ExtID: ""} if _, err := db.Users.c.Insert(*user); err != nil { t.Fatal(err) @@ -355,22 +347,339 @@ func TestFullMapIndex(t *testing.T) { t.Fatal(err) } + err = db.Users.emailMap.Update("x@y.com", func(u User) (User, error) { + u.Email = "a@b.com" + return u, nil + }) + if !errors.Is(err, ErrDuplicate) { + t.Fatal(err) + } + return users }) - // update conflict } func TestPartialMapIndex(t *testing.T) { - // insert into index - // insert into index conflict - // insert outside index - // insert and delete in index - // insert and delete outside index - // update outside index - // update into index - // update function error - // udpate ErrAbortUpdate - // update ErrNotFound - // update conflict in to in - // update conflict out to in + // Test against the extID map index. + run := func(name string, inner func(t *testing.T, db *DB) map[string]*User) { + testWithDB(t, name, func(t *testing.T, db *DB) { + expected := inner(t, db) + + if err := db.Users.extIDMap.EqualsMap(expected); err != nil { + t.Fatal(err) + } + + db.Close() + db = OpenDB(db.root, true) + + if err := db.Users.extIDMap.EqualsMap(expected); err != nil { + t.Fatal(err) + } + }) + } + + run("insert", func(t *testing.T, db *DB) map[string]*User { + users := map[string]*User{ + "x": {ID: db.Users.c.NextID(), Email: "x@y.com", Name: "x", ExtID: "x"}, + } + user1 := &User{ID: db.Users.c.NextID(), Email: "a@b.com", Name: "a"} + + if _, err := db.Users.c.Insert(*users["x"]); err != nil { + t.Fatal(err) + } + if _, err := db.Users.c.Insert(*user1); err != nil { + t.Fatal(err) + } + + return users + }) + + run("insert with conflict", func(t *testing.T, db *DB) map[string]*User { + users := map[string]*User{ + "x": {ID: db.Users.c.NextID(), Email: "x@y.com", Name: "x", ExtID: "x"}, + } + user1 := &User{ID: db.Users.c.NextID(), Email: "a@b.com", Name: "y", ExtID: "x"} + + if _, err := db.Users.c.Insert(*users["x"]); err != nil { + t.Fatal(err) + } + if _, err := db.Users.c.Insert(*user1); !errors.Is(err, ErrDuplicate) { + t.Fatal(err) + } + + return users + }) + + run("insert and delete in index", func(t *testing.T, db *DB) map[string]*User { + users := map[string]*User{ + "x": {ID: db.Users.c.NextID(), Email: "x@y.com", Name: "aa", ExtID: "x"}, + "y": {ID: db.Users.c.NextID(), Email: "q@r.com", Name: "bb", ExtID: "y"}, + "z": {ID: db.Users.c.NextID(), Email: "a@b.com", Name: "cc", ExtID: "z"}, + } + + for _, u := range users { + if _, err := db.Users.c.Insert(*u); err != nil { + t.Fatal(err) + } + } + + // Delete from index and from collection. + db.Users.extIDMap.Delete("x") + db.Users.c.Delete(users["z"].ID) + + delete(users, "x") + delete(users, "z") + + return users + }) + + run("insert and delete outside index", func(t *testing.T, db *DB) map[string]*User { + users := map[string]*User{ + "x": {ID: db.Users.c.NextID(), Email: "x@y.com", Name: "aa", ExtID: "x"}, + "y": {ID: db.Users.c.NextID(), Email: "q@r.com", Name: "bb", ExtID: "y"}, + "z": {ID: db.Users.c.NextID(), Email: "a@b.com", Name: "cc"}, + } + + for _, u := range users { + if _, err := db.Users.c.Insert(*u); err != nil { + t.Fatal(err) + } + } + + // Delete from index and from collection. + db.Users.extIDMap.Delete("x") + db.Users.c.Delete(users["z"].ID) + + delete(users, "x") + delete(users, "z") + + return users + }) + + run("update outside index", func(t *testing.T, db *DB) map[string]*User { + users := map[string]*User{ + "x": {ID: db.Users.c.NextID(), Email: "x@y.com", Name: "aa", ExtID: "x"}, + "y": {ID: db.Users.c.NextID(), Email: "q@r.com", Name: "bb", ExtID: "y"}, + "z": {ID: db.Users.c.NextID(), Email: "a@b.com", Name: "cc"}, + } + + for _, u := range users { + if _, err := db.Users.c.Insert(*u); err != nil { + t.Fatal(err) + } + } + + err := db.Users.c.Update(users["z"].ID, func(u User) (User, error) { + u.Name = "Whatever" + return u, nil + }) + if err != nil { + t.Fatal(err) + } + + delete(users, "z") // No ExtID => not in index. + return users + }) + + run("update into index", func(t *testing.T, db *DB) map[string]*User { + users := map[string]*User{ + "x": {ID: db.Users.c.NextID(), Email: "x@y.com", Name: "aa", ExtID: "x"}, + "y": {ID: db.Users.c.NextID(), Email: "q@r.com", Name: "bb", ExtID: "y"}, + "z": {ID: db.Users.c.NextID(), Email: "a@b.com", Name: "cc"}, + } + + for _, u := range users { + if _, err := db.Users.c.Insert(*u); err != nil { + t.Fatal(err) + } + } + + err := db.Users.c.Update(users["z"].ID, func(u User) (User, error) { + u.ExtID = "z" + return u, nil + }) + if err != nil { + t.Fatal(err) + } + + users["z"].ExtID = "z" + + return users + }) + + run("update out of index", func(t *testing.T, db *DB) map[string]*User { + users := map[string]*User{ + "x": {ID: db.Users.c.NextID(), Email: "x@y.com", Name: "aa", ExtID: "x"}, + "y": {ID: db.Users.c.NextID(), Email: "q@r.com", Name: "bb", ExtID: "y"}, + "z": {ID: db.Users.c.NextID(), Email: "a@b.com", Name: "cc"}, + } + + for _, u := range users { + if _, err := db.Users.c.Insert(*u); err != nil { + t.Fatal(err) + } + } + + err := db.Users.extIDMap.Update("y", func(u User) (User, error) { + u.ExtID = "" + return u, nil + }) + if err != nil { + t.Fatal(err) + } + + err = db.Users.c.Update(users["x"].ID, func(u User) (User, error) { + u.ExtID = "" + return u, nil + }) + if err != nil { + t.Fatal(err) + } + + delete(users, "x") + delete(users, "z") + delete(users, "y") + + return users + }) + + run("update function error", func(t *testing.T, db *DB) map[string]*User { + users := map[string]*User{ + "x": {ID: db.Users.c.NextID(), Email: "x@y.com", Name: "aa", ExtID: "x"}, + "y": {ID: db.Users.c.NextID(), Email: "q@r.com", Name: "bb", ExtID: "y"}, + } + + for _, u := range users { + if _, err := db.Users.c.Insert(*u); err != nil { + t.Fatal(err) + } + } + + myErr := errors.New("hello") + + err := db.Users.extIDMap.Update("y", func(u User) (User, error) { + u.Email = "blah" + return u, myErr + }) + if err != myErr { + t.Fatal(err) + } + + return users + }) + + run("update ErrAbortUpdate", func(t *testing.T, db *DB) map[string]*User { + users := map[string]*User{ + "x": {ID: db.Users.c.NextID(), Email: "x@y.com", Name: "aa", ExtID: "x"}, + "y": {ID: db.Users.c.NextID(), Email: "q@r.com", Name: "bb", ExtID: "y"}, + } + + for _, u := range users { + if _, err := db.Users.c.Insert(*u); err != nil { + t.Fatal(err) + } + } + + err := db.Users.extIDMap.Update("y", func(u User) (User, error) { + u.Email = "blah" + return u, ErrAbortUpdate + }) + if err != nil { + t.Fatal(err) + } + + return users + }) + + run("update ErrNotFound", func(t *testing.T, db *DB) map[string]*User { + users := map[string]*User{ + "x": {ID: db.Users.c.NextID(), Email: "x@y.com", Name: "aa", ExtID: "x"}, + "y": {ID: db.Users.c.NextID(), Email: "q@r.com", Name: "bb", ExtID: "y"}, + } + + for _, u := range users { + if _, err := db.Users.c.Insert(*u); err != nil { + t.Fatal(err) + } + } + + err := db.Users.extIDMap.Update("z", func(u User) (User, error) { + u.Name = "blah" + return u, nil + }) + if !errors.Is(err, ErrNotFound) { + t.Fatal(err) + } + + return users + }) + + run("update conflict in to in", func(t *testing.T, db *DB) map[string]*User { + users := map[string]*User{ + "x": {ID: db.Users.c.NextID(), Email: "x@y.com", Name: "aa", ExtID: "x"}, + "y": {ID: db.Users.c.NextID(), Email: "q@r.com", Name: "bb", ExtID: "y"}, + "z": {ID: db.Users.c.NextID(), Email: "z@z.com", Name: "zz", ExtID: "z"}, + } + + for _, u := range users { + if _, err := db.Users.c.Insert(*u); err != nil { + t.Fatal(err) + } + } + + err := db.Users.extIDMap.Update("z", func(u User) (User, error) { + u.ExtID = "x" + return u, nil + }) + if !errors.Is(err, ErrDuplicate) { + t.Fatal(err) + } + + return users + }) + + run("update conflict out to in", func(t *testing.T, db *DB) map[string]*User { + users := map[string]*User{ + "x": {ID: db.Users.c.NextID(), Email: "x@y.com", Name: "aa", ExtID: "x"}, + "y": {ID: db.Users.c.NextID(), Email: "q@r.com", Name: "bb", ExtID: "y"}, + "z": {ID: db.Users.c.NextID(), Email: "z@z.com", Name: "zz"}, + } + + for _, u := range users { + if _, err := db.Users.c.Insert(*u); err != nil { + t.Fatal(err) + } + } + + err := db.Users.c.Update(users["z"].ID, func(u User) (User, error) { + u.ExtID = "x" + return u, nil + }) + if !errors.Is(err, ErrDuplicate) { + t.Fatal(err) + } + + delete(users, "z") + + return users + }) +} + +func TestMapIndex_load_ErrDuplicate(t *testing.T) { + testWithDB(t, "", func(t *testing.T, db *DB) { + idx := NewMapIndex( + db.Users.c, + "broken", + func(u *User) string { return u.Name }, + nil) + + users := map[uint64]*User{ + 1: {ID: 1, Email: "x@y.com", Name: "aa", ExtID: "x"}, + 2: {ID: 2, Email: "b@c.com", Name: "aa", ExtID: "y"}, + } + + if err := idx.load(users); err != ErrDuplicate { + t.Fatal(err) + } + }) }