Skip to content

Commit

Permalink
feat: finish gmm
Browse files Browse the repository at this point in the history
  • Loading branch information
wangjiahan committed Aug 22, 2024
1 parent e91263a commit ea8f4a0
Show file tree
Hide file tree
Showing 18 changed files with 2,412 additions and 76 deletions.
2 changes: 1 addition & 1 deletion _typos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
configer = "configer"

[files]
extend-exclude = ["CHANGELOG.md", "notebooks/*", ".gitignore", "assets/*"]
extend-exclude = ["CHANGELOG.md", "notebooks/*", ".gitignore", "assets/*", "design/*"]
252 changes: 252 additions & 0 deletions builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
package gmm

import (
"database/sql"
"errors"
"fmt"
"log/slog"
"os"
"reflect"
"strconv"
"sync/atomic"

"github.com/dolthub/go-mysql-server/server"
"github.com/google/uuid"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)

var port atomic.Int32

func init() {
port.Store(19527)
}

type GMMBuilder struct {
dbName string
port int
server *server.Server
sqlDB *sql.DB
gormDB *gorm.DB
err error

tables []schema.Tabler
models []schema.Tabler
sqlStmts []string
sqlFiles []string
}

func Builder(db ...string) *GMMBuilder {
b := &GMMBuilder{
port: int(port.Add(1)),
tables: make([]schema.Tabler, 0),
models: make([]schema.Tabler, 0),
sqlStmts: make([]string, 0),
sqlFiles: make([]string, 0),
}
dbName := "gmm-test-db-" + uuid.NewString()[:6]
if len(db) > 0 {
dbName = db[0]
}
b.dbName = dbName
return b
}

func (b *GMMBuilder) Port(port int) *GMMBuilder {
if b.err != nil {
return b
}
b.port = port
return b
}

func (b *GMMBuilder) Build() (sDB *sql.DB, gDB *gorm.DB, shutdown func(), err error) {
b.initServer()
if b.err != nil {
return nil, nil, nil, b.err
}

// start server
slog.Info("start go mysql mocker server, listening at 0.0.0.0:" + strconv.Itoa(b.port))
go func() {
if err := b.server.Start(); err != nil {
panic(err)
}
}()

shutdown = func() {
_ = b.server.Close()
}

// create client and connect to server
b.sqlDB, b.gormDB, err = createMySQLClient(b.port, b.dbName)
if err != nil {
b.err = fmt.Errorf("failed to create sql client: %w", err)
return nil, nil, nil, b.err
}

// prepare init data
b.initTables()
b.initWithModels()
b.initWithStmts()
b.initWithFiles()
if b.err != nil {
return nil, nil, nil, b.err
}

return b.sqlDB, b.gormDB, shutdown, nil
}

func (b *GMMBuilder) initServer() *GMMBuilder {
if b.err != nil {
return b
}
b.server, b.err = createMySQLServer(b.dbName, b.port)
return b
}

func (b *GMMBuilder) CreateTable(table schema.Tabler) *GMMBuilder {
if b.err != nil {
return b
}
b.tables = append(b.tables, table)
return b
}

// InitData adds init data, if data is a struct/pointer, should implement schema.Tabler.
// if data is a slice, each item should implement schema.Tabler, otherwise will return error.
func (b *GMMBuilder) InitData(data interface{}) *GMMBuilder {
if b.err != nil {
return b
}

if reflect.TypeOf(data).Kind() == reflect.Slice {
slice := reflect.ValueOf(data)
for i := 0; i < slice.Len(); i++ {
item := slice.Index(i).Interface()
model, ok := item.(schema.Tabler)
if !ok {
b.err = errors.New("every single data should implement gorm schema.Tabler")
return b
}
b.models = append(b.models, model)
}
return b
}

model, ok := data.(schema.Tabler)
if !ok {
b.err = errors.New("data should implement gorm schema.Tabler")
return b
}
b.models = append(b.models, model)
return b
}

// SQLStmts adds sql statements, each statement should be a valid sql statement.
func (b *GMMBuilder) SQLStmts(stmts ...string) *GMMBuilder {
if b.err != nil {
return b
}
b.sqlStmts = append(b.sqlStmts, stmts...)
return b
}

// SQLFiles adds sql files, each file should be a valid sql file.
func (b *GMMBuilder) SQLFiles(files ...string) *GMMBuilder {
if b.err != nil {
return b
}

for _, file := range files {
if _, err := os.Stat(file); os.IsNotExist(err) {
b.err = fmt.Errorf("sql file %s not exist", file)
return b
}
}

b.sqlFiles = append(b.sqlFiles, files...)
return b
}

func (b *GMMBuilder) initTables() {
if b.err != nil || len(b.tables) == 0 {
return
}

slog.Info("start to init tables, count = " + strconv.Itoa(len(b.tables)))
for _, table := range b.tables {
if err := b.gormDB.AutoMigrate(table); err != nil {
b.err = fmt.Errorf("failed to auto migrate(type=%T): %w", table, err)
return
}
}
slog.Info("init tables successfully, count = " + strconv.Itoa(len(b.tables)))
}

func (b *GMMBuilder) initWithModels() {
if b.err != nil || len(b.models) == 0 {
return
}

slog.Info("start to init data with models, count = " + strconv.Itoa(len(b.models)))
for _, model := range b.models {
if err := b.gormDB.AutoMigrate(model); err != nil {
b.err = fmt.Errorf("failed to auto migrate(type=%T): %w", model, err)
return
}
if err := b.gormDB.Create(model).Error; err != nil {
b.err = fmt.Errorf("failed to init data(type=%T): %w", model, err)
return
}
}
slog.Info("init data with models successfully, count = " + strconv.Itoa(len(b.models)))
}

func (b *GMMBuilder) initWithStmts() {
if b.err != nil || len(b.sqlStmts) == 0 {
return
}
slog.Info("start to init data with sql stmts, count = " + strconv.Itoa(len(b.sqlStmts)))
for _, stmt := range b.sqlStmts {
stmts, err := splitSQLStatements(stmt)
if err != nil {
b.err = err
return
}
if err = b.executeSQLStatements(stmts); err != nil {
b.err = err
return
}
}
slog.Info("init data with sql stmts successfully, count = " + strconv.Itoa(len(b.sqlStmts)))
}

func (b *GMMBuilder) initWithFiles() {
if b.err != nil || len(b.sqlFiles) == 0 {
return
}
slog.Info("start to init data with sql files, count = " + strconv.Itoa(len(b.sqlFiles)))
for _, file := range b.sqlFiles {
stmts, err := splitSQLFile(file)
if err != nil {
b.err = fmt.Errorf("failed to split sql file '%s': %w", file, err)
return
}
if err = b.executeSQLStatements(stmts); err != nil {
b.err = err
return
}
}
slog.Info("init data with sql files successfully, count = " + strconv.Itoa(len(b.sqlFiles)))
}

func (b *GMMBuilder) executeSQLStatements(stmts []string) error {
for _, stmt := range stmts {
_, err := b.sqlDB.Exec(stmt)
if err != nil {
return fmt.Errorf("failed to exec sql stmt '%s': %w", stmt, err)
}
}
return nil
}
123 changes: 123 additions & 0 deletions builder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package gmm

import (
"testing"

"github.com/stretchr/testify/assert"
)

type CertificationInfo struct {
ID int `gorm:"column:id;index;primaryKey;autoIncrement"`
Username string `gorm:"column:username"`
Password string `gorm:"column:password"`
}

func (receiver CertificationInfo) TableName() string {
return "certification_info"
}

type UserState struct {
UID string `gorm:"primaryKey;column:uid"`
State string `gorm:"column:state"`
}

func (receiver UserState) TableName() string {
return "user_state"
}

type UnknownType struct{}

func Test_GMMBuilder_Port(t *testing.T) {
t.Run("listening on same port should return err", func(t *testing.T) {
_, _, _, err := Builder().Port(9527).Build()
assert.Nil(t, err)
_, _, _, err = Builder().Port(9527).Build()
assert.NotNil(t, err)
assert.Equal(t, "failed to create server: Port 127.0.0.1:9527 already in use.", err.Error())
})
}

func Test_GMMBuilder_File(t *testing.T) {
t.Run("non existing file should return err", func(t *testing.T) {
_, _, _, err := Builder().SQLFiles("fixtures/not-exist.sql").Build()
assert.NotNil(t, err)
assert.Equal(t, "sql file fixtures/not-exist.sql not exist", err.Error())
})

t.Run("existing and valid file should ok", func(t *testing.T) {
_, _, _, err := Builder().SQLFiles("fixtures/sequel_ace.sql").Build()
assert.Nil(t, err)
})
}

//nolint:lll
func Test_GMMBuilder_Stmt(t *testing.T) {
t.Run("invalid sql statement should return err", func(t *testing.T) {
_, _, _, err := Builder().SQLStmts("invalid sql statement").Build()
assert.NotNil(t, err)
assert.Equal(t, "failed to exec sql stmt 'invalid sql statement': Error 1105 (HY000): syntax error at position 8 near 'invalid'", err.Error())
})

t.Run("valid sql statement should ok", func(t *testing.T) {
_, _, _, err := Builder().CreateTable(CertificationInfo{}).SQLStmts("insert into certification_info(id, username, password) values (1, 'hedon', 'hedon-pwd');").Build()
assert.Nil(t, err)
})

t.Run("multi valid sql statements should ok", func(t *testing.T) {
sql := `
CREATE TABLE IF NOT EXISTS users (
id INT NOT NULL AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO users (username, email) VALUES
('john_doe', 'john.doe@example.com'),
('jane_smith', 'jane.smith@example.com'),
('alice_jones', 'alice.jones@example.com');`
_, _, _, err := Builder().SQLStmts(sql).Build()
assert.Nil(t, err)
})
}

func TestGMMBuilder_Data(t *testing.T) {
t.Run("invalid data type should return err", func(t *testing.T) {
_, _, _, err := Builder().InitData(1).Build()
assert.NotNil(t, err)
assert.Equal(t, "data should implement gorm schema.Tabler", err.Error())
})

t.Run("auto increment, but struct is unaddressable, should panic", func(t *testing.T) {
assert.Panics(t, func() {
_, _, _, _ = Builder().InitData(CertificationInfo{}).Build()
})
})

t.Run("struct is unaddressable, but would not be changed, should ok", func(t *testing.T) {
_, _, _, err := Builder().InitData(UserState{}).Build()
assert.Nil(t, err)
})

t.Run("type not implemented schema.Tabler, should return error", func(t *testing.T) {
_, _, _, err := Builder().InitData(&UnknownType{}).Build()
assert.NotNil(t, err)
assert.Equal(t, "data should implement gorm schema.Tabler", err.Error())
})

t.Run("pointer implemented schema.Tabler should ok", func(t *testing.T) {
_, _, _, err := Builder().InitData(&CertificationInfo{}).Build()
assert.Nil(t, err)
})

t.Run("slice implemented schema.Tabler should ok", func(t *testing.T) {
_, _, _, err := Builder().InitData([]CertificationInfo{}).Build()
assert.Nil(t, err)
})

t.Run("slice of different types that implemented schema.Tabler should ok", func(t *testing.T) {
_, _, _, err := Builder().InitData([]interface{}{&CertificationInfo{}, &UserState{}}).Build()
assert.Nil(t, err)
})
}
Loading

0 comments on commit ea8f4a0

Please sign in to comment.