This repository has been archived on 2022-07-30. You can view files and clone it, but cannot push or open issues/pull-requests.
mdb/testdb_test.go

269 lines
5.8 KiB
Go

package mdb
import (
"errors"
"fmt"
"math/rand"
"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,
"email-bt",
func(lhs, rhs *User) bool { return lhs.Email < rhs.Email },
nil)
db.Users.nameBTree = NewBTreeIndex(
db.Users.c,
"name-bt",
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,
"extid-bt",
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)
db.Accounts.nameMap = NewMapIndex(
db.Accounts.c,
"name",
func(a *Account) string { return a.Name },
nil)
db.Start()
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 fmt.Errorf("%w: Users.c.items not equal", err)
}
// Users: emailMap
if err := db.Users.emailMap.Equals(rhs.Users.emailMap); err != nil {
return fmt.Errorf("%w: Users.emailMap not equal", err)
}
// Users: emailBTree
if err := db.Users.emailBTree.Equals(rhs.Users.emailBTree); err != nil {
return fmt.Errorf("%w: Users.emailBTree not equal", err)
}
// Users: nameBTree
if err := db.Users.nameBTree.Equals(rhs.Users.nameBTree); err != nil {
return fmt.Errorf("%w: Users.nameBTree not equal", err)
}
// Users: extIDMap
if err := db.Users.extIDMap.Equals(rhs.Users.extIDMap); err != nil {
return fmt.Errorf("%w: Users.extIDMap not equal", err)
}
// Users: extIDBTree
if err := db.Users.extIDBTree.Equals(rhs.Users.extIDBTree); err != nil {
return fmt.Errorf("%w: Users.extIDBTree not equal", err)
}
// Accounts: itemMap
if err := db.Accounts.c.items.Equals(rhs.Accounts.c.items); err != nil {
return fmt.Errorf("%w: Accounts.c.items not equal", err)
}
// Accounts: nameMap
if err := db.Accounts.nameMap.Equals(rhs.Accounts.nameMap); err != nil {
return fmt.Errorf("%w: Accounts.nameMap not equal", err)
}
return nil
}
// Wait for two databases to become synchronized.
func (db *DB) WaitForSync(rhs *DB) {
for {
if db.MaxSeqNum() == rhs.MaxSeqNum() {
return
}
time.Sleep(100 * time.Millisecond)
}
}
var (
randIDs = []uint64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 2, 13, 14, 15, 16}
)
func (db *DB) RandAction() {
if rand.Float32() < 0.3 {
db.randActionAccount()
} else {
db.randActionUser()
}
}
func (db *DB) randActionAccount() {
id := randIDs[rand.Intn(len(randIDs))]
f := rand.Float32()
_, exists := db.Accounts.c.Get(id)
if !exists {
db.Accounts.c.Insert(Account{
ID: id,
Name: randString(),
})
return
}
if f < 0.05 {
db.Accounts.c.Delete(id)
return
}
db.Accounts.c.Update(id, func(a Account) (Account, error) {
a.Name = randString()
return a, nil
})
}
func (db *DB) randActionUser() {
id := randIDs[rand.Intn(len(randIDs))]
f := rand.Float32()
_, exists := db.Users.c.Get(id)
if !exists {
user := User{
ID: id,
Email: randString() + "@domain.com",
Name: randString(),
}
if f < 0.1 {
user.ExtID = randString()
}
db.Users.c.Insert(user)
return
}
if f < 0.05 {
db.Users.c.Delete(id)
return
}
db.Users.c.Update(id, func(a User) (User, error) {
a.Name = randString()
if f < 0.1 {
a.ExtID = randString()
} else {
a.ExtID = ""
}
a.Email = randString() + "@domain.com"
return a, nil
})
}