WIP: testing
parent
32b0618505
commit
dce352161b
|
@ -4,8 +4,8 @@ An in-process, in-memory database for Go.
|
||||||
|
|
||||||
## TO DO
|
## TO DO
|
||||||
|
|
||||||
* [ ] mdb: db exclusive lock
|
* MDB Tests
|
||||||
* [ ] mdb: clean up tests
|
*
|
||||||
|
|
||||||
## Structure
|
## Structure
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
package mdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (bt *BTreeIndex[T]) Equals(rhs *BTreeIndex[T]) error {
|
||||||
|
if bt.Len() != rhs.Len() {
|
||||||
|
return fmt.Errorf("Expected %d items, but found %d.", bt.Len(), rhs.Len())
|
||||||
|
}
|
||||||
|
|
||||||
|
it1 := bt.Ascend()
|
||||||
|
defer it1.Close()
|
||||||
|
|
||||||
|
it2 := rhs.Ascend()
|
||||||
|
defer it2.Close()
|
||||||
|
|
||||||
|
for it1.Next() {
|
||||||
|
it2.Next()
|
||||||
|
|
||||||
|
v1 := it1.Value()
|
||||||
|
v2 := it2.Value()
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(v1, v2) {
|
||||||
|
return fmt.Errorf("Value mismatch: %v != %v", v1, v2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bt *BTreeIndex[T]) EqualsList(data []*T) error {
|
||||||
|
if bt.Len() != len(data) {
|
||||||
|
return fmt.Errorf("Expected %d items, but found %d.", bt.Len(), len(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
it1 := bt.Ascend()
|
||||||
|
defer it1.Close()
|
||||||
|
|
||||||
|
for _, v1 := range data {
|
||||||
|
it1.Next()
|
||||||
|
v2 := it1.Value()
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(v1, v2) {
|
||||||
|
return fmt.Errorf("Value mismatch: %v != %v", v1, v2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -1,13 +1,6 @@
|
||||||
package mdb
|
package mdb
|
||||||
|
|
||||||
import (
|
/*
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestBTreeIndex(t *testing.T) {
|
func TestBTreeIndex(t *testing.T) {
|
||||||
type Item struct {
|
type Item struct {
|
||||||
ID uint64
|
ID uint64
|
||||||
|
@ -380,3 +373,4 @@ func TestBTreeIndex(t *testing.T) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
|
@ -1,17 +1,6 @@
|
||||||
package mdb
|
package mdb
|
||||||
|
|
||||||
import (
|
/*
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCollection(t *testing.T) {
|
func TestCollection(t *testing.T) {
|
||||||
type Item struct {
|
type Item struct {
|
||||||
ID uint64
|
ID uint64
|
||||||
|
@ -424,3 +413,4 @@ func BenchmarkLoad(b *testing.B) {
|
||||||
panic("What?")
|
panic("What?")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
|
@ -2,7 +2,7 @@ package mdb
|
||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
func (db *Database) waitForWAL() {
|
func (db *Database) WaitForWAL() {
|
||||||
for {
|
for {
|
||||||
status := db.WALStatus()
|
status := db.WALStatus()
|
||||||
if status.MaxSeqNumWAL == status.MaxSeqNumKV {
|
if status.MaxSeqNumWAL == status.MaxSeqNumKV {
|
|
@ -0,0 +1,54 @@
|
||||||
|
package mdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *itemMap[T]) Equals(rhs *itemMap[T]) error {
|
||||||
|
return m.EqualsMap(rhs.m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *itemMap[T]) EqualsMap(data map[uint64]*T) error {
|
||||||
|
if len(data) != len(m.m) {
|
||||||
|
return fmt.Errorf("Expected %d items, but found %d.", len(data), len(m.m))
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, exp := range data {
|
||||||
|
val, ok := m.m[key]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("No value for %d. Expected: %v", key, *exp)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(*val, *exp) {
|
||||||
|
return fmt.Errorf("Value mismatch %d: %v != %v", key, *val, *exp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *itemMap[T]) EqualsKV() (err error) {
|
||||||
|
count := 0
|
||||||
|
m.kv.Iterate(m.collection, func(id uint64, data []byte) {
|
||||||
|
count++
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item := decode[T](data)
|
||||||
|
val, ok := m.m[id]
|
||||||
|
if !ok {
|
||||||
|
err = fmt.Errorf("Item %d not found in memory: %v", id, *item)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(*item, *val) {
|
||||||
|
err = fmt.Errorf("Items not equal %d: %v != %v", id, *item, *val)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err == nil && count != len(m.m) {
|
||||||
|
err = fmt.Errorf("%d items on disk, but %d in memory", count, len(m.m))
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
|
@ -0,0 +1,129 @@
|
||||||
|
package mdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestItemMap(t *testing.T) {
|
||||||
|
|
||||||
|
// expected is a map of users.
|
||||||
|
run := func(name string, inner func(t *testing.T, db *DB) (expected map[uint64]*User)) {
|
||||||
|
testWithDB(t, name, func(t *testing.T, db *DB) {
|
||||||
|
expected := inner(t, db)
|
||||||
|
|
||||||
|
if err := db.Users.c.items.EqualsMap(expected); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
db.WaitForWAL()
|
||||||
|
|
||||||
|
if err := db.Users.c.items.EqualsKV(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Close()
|
||||||
|
db = OpenDB(db.root, true)
|
||||||
|
|
||||||
|
if err := db.Users.c.items.EqualsMap(expected); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Users.c.items.EqualsKV(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
run("simple", func(t *testing.T, db *DB) (expected map[uint64]*User) {
|
||||||
|
users := map[uint64]*User{}
|
||||||
|
c := db.Users.c
|
||||||
|
for i := uint64(1); i < 10; i++ {
|
||||||
|
id := c.NextID()
|
||||||
|
users[id] = &User{
|
||||||
|
ID: id,
|
||||||
|
Email: fmt.Sprintf("a.%d@c.com", i),
|
||||||
|
Name: fmt.Sprintf("name.%d", i),
|
||||||
|
ExtID: fmt.Sprintf("EXTID.%d", i),
|
||||||
|
}
|
||||||
|
user, err := c.Insert(*users[id])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(user, *users[id]) {
|
||||||
|
t.Fatal(user, *users[id])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return users
|
||||||
|
})
|
||||||
|
|
||||||
|
run("insert and delete", func(t *testing.T, db *DB) (expected map[uint64]*User) {
|
||||||
|
users := map[uint64]*User{}
|
||||||
|
c := db.Users.c
|
||||||
|
for x := uint64(1); x < 10; x++ {
|
||||||
|
id := c.NextID()
|
||||||
|
users[id] = &User{
|
||||||
|
ID: id,
|
||||||
|
Email: fmt.Sprintf("a.%d@c.com", x),
|
||||||
|
Name: fmt.Sprintf("name.%d", x),
|
||||||
|
ExtID: fmt.Sprintf("EXTID.%d", x),
|
||||||
|
}
|
||||||
|
user, err := c.Insert(*users[id])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(user, *users[id]) {
|
||||||
|
t.Fatal(user, *users[id])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var id uint64
|
||||||
|
for key := range users {
|
||||||
|
id = key
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(users, id)
|
||||||
|
c.Delete(id)
|
||||||
|
|
||||||
|
return users
|
||||||
|
})
|
||||||
|
|
||||||
|
run("update", func(t *testing.T, db *DB) (expected map[uint64]*User) {
|
||||||
|
users := map[uint64]*User{}
|
||||||
|
c := db.Users.c
|
||||||
|
for x := uint64(1); x < 10; x++ {
|
||||||
|
id := c.NextID()
|
||||||
|
users[id] = &User{
|
||||||
|
ID: id,
|
||||||
|
Email: fmt.Sprintf("a.%d@c.com", x),
|
||||||
|
Name: fmt.Sprintf("name.%d", x),
|
||||||
|
ExtID: fmt.Sprintf("EXTID.%d", x),
|
||||||
|
}
|
||||||
|
user, err := c.Insert(*users[id])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(user, *users[id]) {
|
||||||
|
t.Fatal(user, *users[id])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var id uint64
|
||||||
|
for key := range users {
|
||||||
|
id = key
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.Update(id, func(u User) (User, error) {
|
||||||
|
u.Name = "Hello"
|
||||||
|
return u, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
users[id].Name = "Hello"
|
||||||
|
|
||||||
|
return users
|
||||||
|
})
|
||||||
|
}
|
14
main_test.go
14
main_test.go
|
@ -11,3 +11,17 @@ func TestMain(m *testing.M) {
|
||||||
rand.Seed(time.Now().UnixNano())
|
rand.Seed(time.Now().UnixNano())
|
||||||
os.Exit(m.Run())
|
os.Exit(m.Run())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testWithDB(t *testing.T, name string, inner func(t *testing.T, db *DB)) {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
root, err := os.MkdirTemp("", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(root)
|
||||||
|
|
||||||
|
db := OpenDB(root, true)
|
||||||
|
defer db.Close()
|
||||||
|
inner(t, db)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
package mdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *MapIndex[K, T]) Equals(rhs *MapIndex[K, T]) error {
|
||||||
|
return m.EqualsMap(rhs.m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MapIndex[K, T]) EqualsMap(data map[K]*T) error {
|
||||||
|
if len(m.m) != len(data) {
|
||||||
|
return fmt.Errorf("Expected %d items, but found %d.", len(data), len(m.m))
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, exp := range data {
|
||||||
|
val, ok := m.m[key]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("No value for %v. Expected: %v", key, *exp)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(*val, *exp) {
|
||||||
|
return fmt.Errorf("Value mismatch %v: %v != %v", key, *val, *exp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -1,14 +1,6 @@
|
||||||
package mdb
|
package mdb
|
||||||
|
|
||||||
import (
|
/*
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMapIndex(t *testing.T) {
|
func TestMapIndex(t *testing.T) {
|
||||||
type Item struct {
|
type Item struct {
|
||||||
ID uint64
|
ID uint64
|
||||||
|
@ -328,3 +320,4 @@ func TestMapIndexLoadError(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
package mdb
|
package mdb
|
||||||
|
|
||||||
import (
|
/*
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLogShip(t *testing.T) {
|
func TestLogShip(t *testing.T) {
|
||||||
type Item struct {
|
type Item struct {
|
||||||
ID uint64
|
ID uint64
|
||||||
|
@ -79,3 +73,4 @@ func TestLogShip(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
|
@ -0,0 +1,187 @@
|
||||||
|
package mdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/mail"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Validate errors.
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
var ErrInvalidName = errors.New("invalid name")
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// User Collection
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
ID uint64
|
||||||
|
Email string
|
||||||
|
Name string
|
||||||
|
ExtID string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Users struct {
|
||||||
|
c *Collection[User]
|
||||||
|
emailMap *MapIndex[string, User] // Full map index.
|
||||||
|
emailBTree *BTreeIndex[User] // Full btree index.
|
||||||
|
nameBTree *BTreeIndex[User] // Full btree with duplicates.
|
||||||
|
extIDMap *MapIndex[string, User] // Partial map index.
|
||||||
|
extIDBTree *BTreeIndex[User] // Partial btree index.
|
||||||
|
}
|
||||||
|
|
||||||
|
func userGetID(u *User) uint64 { return u.ID }
|
||||||
|
|
||||||
|
func userSanitize(u *User) {
|
||||||
|
u.Name = strings.TrimSpace(u.Name)
|
||||||
|
e, err := mail.ParseAddress(strings.ToLower(strings.TrimSpace(u.Email)))
|
||||||
|
if err == nil {
|
||||||
|
u.Email = e.Address
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func userValidate(u *User) error {
|
||||||
|
if len(u.Name) == 0 {
|
||||||
|
return ErrInvalidName
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Account Collection
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Account struct {
|
||||||
|
ID uint64
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Accounts struct {
|
||||||
|
c *Collection[Account]
|
||||||
|
nameMap *MapIndex[string, Account]
|
||||||
|
}
|
||||||
|
|
||||||
|
func accountGetID(a *Account) uint64 { return a.ID }
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Database
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type DB struct {
|
||||||
|
*Database
|
||||||
|
root string
|
||||||
|
Users Users
|
||||||
|
Accounts Accounts
|
||||||
|
}
|
||||||
|
|
||||||
|
func OpenDB(root string, primary bool) *DB {
|
||||||
|
db := &DB{root: root}
|
||||||
|
if primary {
|
||||||
|
db.Database = NewPrimary(root)
|
||||||
|
} else {
|
||||||
|
db.Database = NewSecondary(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Users = Users{}
|
||||||
|
db.Users.c = NewCollection(db.Database, "users", userGetID)
|
||||||
|
db.Users.c.SetSanitize(userSanitize)
|
||||||
|
db.Users.c.SetValidate(userValidate)
|
||||||
|
|
||||||
|
db.Users.emailMap = NewMapIndex(
|
||||||
|
db.Users.c,
|
||||||
|
"email",
|
||||||
|
func(u *User) string { return u.Email },
|
||||||
|
nil)
|
||||||
|
|
||||||
|
db.Users.emailBTree = NewBTreeIndex(
|
||||||
|
db.Users.c,
|
||||||
|
func(lhs, rhs *User) bool { return lhs.Email < rhs.Email },
|
||||||
|
nil)
|
||||||
|
|
||||||
|
db.Users.nameBTree = NewBTreeIndex(
|
||||||
|
db.Users.c,
|
||||||
|
func(lhs, rhs *User) bool {
|
||||||
|
if lhs.Name != rhs.Name {
|
||||||
|
return lhs.Name < rhs.Name
|
||||||
|
}
|
||||||
|
return lhs.ID < rhs.ID
|
||||||
|
},
|
||||||
|
nil)
|
||||||
|
|
||||||
|
db.Users.extIDMap = NewMapIndex(
|
||||||
|
db.Users.c,
|
||||||
|
"extID",
|
||||||
|
func(u *User) string { return u.ExtID },
|
||||||
|
func(u *User) bool { return u.ExtID != "" })
|
||||||
|
|
||||||
|
db.Users.extIDBTree = NewBTreeIndex(
|
||||||
|
db.Users.c,
|
||||||
|
func(lhs, rhs *User) bool { return lhs.ExtID < rhs.ExtID },
|
||||||
|
func(u *User) bool { return u.ExtID != "" })
|
||||||
|
|
||||||
|
db.Accounts = Accounts{}
|
||||||
|
db.Accounts.c = NewCollection(db.Database, "accounts", accountGetID)
|
||||||
|
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) Equals(rhs *DB) error {
|
||||||
|
db.WaitForSync(rhs)
|
||||||
|
|
||||||
|
// Users: itemMap.
|
||||||
|
if err := db.Users.c.items.Equals(rhs.Users.c.items); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Users: emailMap
|
||||||
|
if err := db.Users.emailMap.Equals(rhs.Users.emailMap); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Users: emailBTree
|
||||||
|
if err := db.Users.emailBTree.Equals(rhs.Users.emailBTree); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Users: nameBTree
|
||||||
|
if err := db.Users.nameBTree.Equals(rhs.Users.nameBTree); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Users: extIDMap
|
||||||
|
if err := db.Users.extIDMap.Equals(rhs.Users.extIDMap); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Users: extIDBTree
|
||||||
|
if err := db.Users.extIDBTree.Equals(rhs.Users.extIDBTree); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accounts: itemMap
|
||||||
|
if err := db.Accounts.c.items.Equals(rhs.Accounts.c.items); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accounts: nameMap
|
||||||
|
if err := db.Accounts.nameMap.Equals(rhs.Accounts.nameMap); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for two databases to become synchronized.
|
||||||
|
func (db *DB) WaitForSync(rhs *DB) {
|
||||||
|
for {
|
||||||
|
s1 := db.WALStatus()
|
||||||
|
s2 := rhs.WALStatus()
|
||||||
|
if s1 == s2 && s1.MaxSeqNumKV == s1.MaxSeqNumWAL {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue