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 }) }