More testing.
parent
e15fe41a50
commit
f4d72247e4
|
@ -5,13 +5,14 @@ An in-process, in-memory database for Go.
|
||||||
## TO DO
|
## TO DO
|
||||||
|
|
||||||
* mapindex_test.go
|
* mapindex_test.go
|
||||||
* TestFullMapIndex
|
* ~~TestFullMapIndex~~
|
||||||
* TestPartialMapIndex
|
|
||||||
* btreeindex_test.go
|
* btreeindex_test.go
|
||||||
* TestFullBTreeIndex
|
|
||||||
* TestPartialBTreeIndex
|
* TestPartialBTreeIndex
|
||||||
* btreeiterator_test.go
|
* btreeiterator_test.go (?)
|
||||||
* collection
|
* collection
|
||||||
* database
|
* database
|
||||||
* WAL shipping
|
* WAL shipping
|
||||||
* WAL shipping with network disconnects
|
* WAL shipping with network disconnects
|
||||||
|
|
||||||
|
* BTreeIndex:
|
||||||
|
* Should insert panic if item is replaced?
|
||||||
|
|
|
@ -29,7 +29,7 @@ func NewBTreeIndex[T any](
|
||||||
include: include,
|
include: include,
|
||||||
}
|
}
|
||||||
|
|
||||||
btree := btree.NewG(64, less)
|
btree := btree.NewG(32, less)
|
||||||
t.bt.Store(btree)
|
t.bt.Store(btree)
|
||||||
c.indices = append(c.indices, t)
|
c.indices = append(c.indices, t)
|
||||||
|
|
||||||
|
|
|
@ -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))
|
return fmt.Errorf("Expected %d items, but found %d.", bt.Len(), len(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
it1 := bt.Ascend()
|
if len(data) == 0 {
|
||||||
defer it1.Close()
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ascend fully.
|
||||||
|
it := bt.Ascend()
|
||||||
for _, v1 := range data {
|
for _, v1 := range data {
|
||||||
it1.Next()
|
it.Next()
|
||||||
v2 := it1.Value()
|
v2 := it.Value()
|
||||||
|
|
||||||
if !reflect.DeepEqual(v1, v2) {
|
if !reflect.DeepEqual(*v1, v2) {
|
||||||
return fmt.Errorf("Value mismatch: %v != %v", 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,376 +1,133 @@
|
||||||
package mdb
|
package mdb
|
||||||
|
|
||||||
/*
|
import (
|
||||||
func TestBTreeIndex(t *testing.T) {
|
"reflect"
|
||||||
type Item struct {
|
"testing"
|
||||||
ID uint64
|
)
|
||||||
Name string
|
|
||||||
}
|
|
||||||
|
|
||||||
checkIndexOne := func(idx *BTreeIndex[Item], expected ...Item) error {
|
func TestFullBTreeIndex(t *testing.T) {
|
||||||
if idx.Len() != len(expected) {
|
|
||||||
return fmt.Errorf("Expected %d items but found %d.", len(expected), idx.Len())
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(expected) == 0 {
|
// Test against the email index.
|
||||||
return nil
|
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 {
|
if err := db.Users.emailBTree.EqualsList(expected); err != nil {
|
||||||
item2, ok := idx.Get(item)
|
t.Fatal(err)
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("Missing expected item: %v", item)
|
|
||||||
}
|
}
|
||||||
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]) {
|
run("insert", func(t *testing.T, db *DB) (users []*User) {
|
||||||
if err := checkIndex(idx); err != nil {
|
users = append(users,
|
||||||
t.Fatal(err)
|
&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]) {
|
// Update
|
||||||
item1 := Item{1, "one"}
|
|
||||||
item2 := Item{2, ""}
|
|
||||||
item3 := Item{3, "three"}
|
|
||||||
item4 := Item{4, ""}
|
|
||||||
item5 := Item{5, "five"}
|
|
||||||
|
|
||||||
c.Insert(item1)
|
run("update", func(t *testing.T, db *DB) (users []*User) {
|
||||||
c.Insert(item2)
|
users = append(users,
|
||||||
c.Insert(item3)
|
&User{ID: db.Users.c.NextID(), Email: "a@b.com", Name: "aaa"},
|
||||||
c.Insert(item4)
|
&User{ID: db.Users.c.NextID(), Email: "e@f.com", Name: "eee"},
|
||||||
c.Insert(item5)
|
&User{ID: db.Users.c.NextID(), Email: "c@d.com", Name: "ccc"})
|
||||||
|
|
||||||
if err := checkIndex(idx, item5, item1, item3); err != nil {
|
for _, u := range users {
|
||||||
t.Fatal(err)
|
if _, err := db.Users.c.Insert(*u); 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !reflect.DeepEqual(iter.Value(), item5) {
|
err := db.Users.c.Update(users[2].ID, func(u User) (User, error) {
|
||||||
t.Fatal(iter.Value(), item5)
|
u.Email = "g@h.com"
|
||||||
}
|
return u, nil
|
||||||
})
|
|
||||||
|
|
||||||
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
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
users[2].Email = "g@h.com"
|
||||||
|
|
||||||
item2.Name = "two"
|
return users
|
||||||
|
|
||||||
if err := checkIndex(idx, item5, item1, item3, item2); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
run("update out of index", func(t *testing.T, c *Collection[Item], idx *BTreeIndex[Item]) {
|
run("delete", func(t *testing.T, db *DB) (users []*User) {
|
||||||
item1 := Item{1, "one"}
|
users = append(users,
|
||||||
item2 := Item{2, ""}
|
&User{ID: db.Users.c.NextID(), Email: "a@b.com", Name: "aaa"},
|
||||||
item3 := Item{3, "three"}
|
&User{ID: db.Users.c.NextID(), Email: "c@d.com", Name: "ccc"},
|
||||||
item4 := Item{4, ""}
|
&User{ID: db.Users.c.NextID(), Email: "e@f.com", Name: "eee"})
|
||||||
item5 := Item{5, "five"}
|
|
||||||
|
|
||||||
c.Insert(item1)
|
for _, u := range users {
|
||||||
c.Insert(item2)
|
if _, err := db.Users.c.Insert(*u); err != nil {
|
||||||
c.Insert(item3)
|
t.Fatal(err)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := checkIndex(idx, item5, item3); err != nil {
|
db.Users.c.Delete(users[0].ID)
|
||||||
t.Fatal(err)
|
users = users[1:]
|
||||||
}
|
|
||||||
|
return users
|
||||||
})
|
})
|
||||||
|
|
||||||
run("update within index", func(t *testing.T, c *Collection[Item], idx *BTreeIndex[Item]) {
|
run("get not found", func(t *testing.T, db *DB) (users []*User) {
|
||||||
item1 := Item{1, "one"}
|
users = append(users,
|
||||||
item2 := Item{2, ""}
|
&User{ID: db.Users.c.NextID(), Email: "a@b.com", Name: "aaa"},
|
||||||
item3 := Item{3, "three"}
|
&User{ID: db.Users.c.NextID(), Email: "c@d.com", Name: "ccc"},
|
||||||
item4 := Item{4, ""}
|
&User{ID: db.Users.c.NextID(), Email: "e@f.com", Name: "eee"})
|
||||||
item5 := Item{5, "five"}
|
|
||||||
|
|
||||||
c.Insert(item1)
|
for _, u := range users {
|
||||||
c.Insert(item2)
|
if _, err := db.Users.c.Insert(*u); err != nil {
|
||||||
c.Insert(item3)
|
t.Fatal(err)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
item1.Name = "xone"
|
if u, ok := db.Users.emailBTree.Get(User{Email: "g@h.com"}); ok {
|
||||||
|
t.Fatal(u, ok)
|
||||||
if err := checkIndex(idx, item5, item3, item1); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return users
|
||||||
})
|
})
|
||||||
|
|
||||||
run("delete outside index", func(t *testing.T, c *Collection[Item], idx *BTreeIndex[Item]) {
|
run("min/max empty", func(t *testing.T, db *DB) (users []*User) {
|
||||||
item1 := Item{1, "one"}
|
|
||||||
item2 := Item{2, ""}
|
|
||||||
item3 := Item{3, "three"}
|
|
||||||
item4 := Item{4, ""}
|
|
||||||
item5 := Item{5, "five"}
|
|
||||||
|
|
||||||
c.Insert(item1)
|
if u, ok := db.Users.emailBTree.Min(); ok {
|
||||||
c.Insert(item2)
|
t.Fatal(u, ok)
|
||||||
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.Max(); 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -72,10 +72,6 @@ func (m *MapIndex[K, T]) Update(k K, update func(T) (T, error)) error {
|
||||||
return item, err
|
return item, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.getKey(&new) != k {
|
|
||||||
return item, ErrMismatchedIDs
|
|
||||||
}
|
|
||||||
|
|
||||||
return new, nil
|
return new, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
357
mapindex_test.go
357
mapindex_test.go
|
@ -51,8 +51,6 @@ func TestFullMapIndex(t *testing.T) {
|
||||||
return users
|
return users
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: insert duplicate
|
|
||||||
|
|
||||||
run("delete", func(t *testing.T, db *DB) map[string]*User {
|
run("delete", func(t *testing.T, db *DB) map[string]*User {
|
||||||
users := map[string]*User{}
|
users := map[string]*User{}
|
||||||
|
|
||||||
|
@ -171,7 +169,7 @@ func TestFullMapIndex(t *testing.T) {
|
||||||
return users
|
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{}
|
users := map[string]*User{}
|
||||||
|
|
||||||
for i := uint64(1); i < 10; i++ {
|
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) {
|
err := db.Users.emailMap.Update(email, func(u User) (User, error) {
|
||||||
u.Email = "test@x.com"
|
u.ID++
|
||||||
return u, nil
|
return u, nil
|
||||||
})
|
})
|
||||||
if err != ErrMismatchedIDs {
|
if err != ErrMismatchedIDs {
|
||||||
|
@ -304,13 +302,7 @@ func TestFullMapIndex(t *testing.T) {
|
||||||
|
|
||||||
run("insert conflict", func(t *testing.T, db *DB) map[string]*User {
|
run("insert conflict", func(t *testing.T, db *DB) map[string]*User {
|
||||||
users := 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 {
|
if _, err := db.Users.c.Insert(*user); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -355,22 +347,339 @@ func TestFullMapIndex(t *testing.T) {
|
||||||
t.Fatal(err)
|
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
|
return users
|
||||||
})
|
})
|
||||||
// update conflict
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPartialMapIndex(t *testing.T) {
|
func TestPartialMapIndex(t *testing.T) {
|
||||||
// insert into index
|
// Test against the extID map index.
|
||||||
// insert into index conflict
|
run := func(name string, inner func(t *testing.T, db *DB) map[string]*User) {
|
||||||
// insert outside index
|
testWithDB(t, name, func(t *testing.T, db *DB) {
|
||||||
// insert and delete in index
|
expected := inner(t, db)
|
||||||
// insert and delete outside index
|
|
||||||
// update outside index
|
if err := db.Users.extIDMap.EqualsMap(expected); err != nil {
|
||||||
// update into index
|
t.Fatal(err)
|
||||||
// update function error
|
}
|
||||||
// udpate ErrAbortUpdate
|
|
||||||
// update ErrNotFound
|
db.Close()
|
||||||
// update conflict in to in
|
db = OpenDB(db.root, true)
|
||||||
// update conflict out to in
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
Reference in New Issue