diff --git a/gee-orm/day6-transaction/clause/clause.go b/gee-orm/day6-transaction/clause/clause.go new file mode 100644 index 0000000..2195c86 --- /dev/null +++ b/gee-orm/day6-transaction/clause/clause.go @@ -0,0 +1,52 @@ +package clause + +import ( + "strings" +) + +// Clause contains SQL conditions +type Clause struct { + sql map[Type]string + sqlVars map[Type][]interface{} +} + +// Type is the type of Clause +type Type int + +// Support types for Clause +const ( + INSERT Type = iota + VALUES + SELECT + LIMIT + WHERE + ORDERBY + UPDATE + SET + DELETE + COUNT +) + +// Set adds a sub clause of specific type +func (c *Clause) Set(name Type, vars ...interface{}) { + if c.sql == nil { + c.sql = make(map[Type]string) + c.sqlVars = make(map[Type][]interface{}) + } + sql, vars := generators[name](vars...) + c.sql[name] = sql + c.sqlVars[name] = vars +} + +// Build generate the final SQL and SQLVars +func (c *Clause) Build(orders ...Type) (string, []interface{}) { + var sqls []string + var vars []interface{} + for _, order := range orders { + if sql, ok := c.sql[order]; ok { + sqls = append(sqls, sql) + vars = append(vars, c.sqlVars[order]...) + } + } + return strings.Join(sqls, " "), vars +} diff --git a/gee-orm/day6-transaction/clause/clause_test.go b/gee-orm/day6-transaction/clause/clause_test.go new file mode 100644 index 0000000..8769c40 --- /dev/null +++ b/gee-orm/day6-transaction/clause/clause_test.go @@ -0,0 +1,76 @@ +package clause + +import ( + "reflect" + "testing" +) + +func TestClause_Set(t *testing.T) { + var clause Clause + clause.Set(INSERT, "User", []string{"Name", "Age"}) + sql := clause.sql[INSERT] + vars := clause.sqlVars[INSERT] + t.Log(sql, vars) + if sql != "INSERT INTO User (Name,Age)" || len(vars) != 0 { + t.Fatal("failed to get clause") + } +} + +func testSelect(t *testing.T) { + var clause Clause + clause.Set(LIMIT, 3) + clause.Set(SELECT, "User", []string{"*"}) + clause.Set(WHERE, "Name = ?", "Tom") + clause.Set(ORDERBY, "Age ASC") + sql, vars := clause.Build(SELECT, WHERE, ORDERBY, LIMIT) + t.Log(sql, vars) + if sql != "SELECT * FROM User WHERE Name = ? ORDER BY Age ASC LIMIT ?" { + t.Fatal("failed to build SQL") + } + if !reflect.DeepEqual(vars, []interface{}{"Tom", 3}) { + t.Fatal("failed to build SQLVars") + } +} + +func testUpdate(t *testing.T) { + var clause Clause + clause.Set(UPDATE, "User") + clause.Set(WHERE, "Name = ?", "Tom") + clause.Set(SET, map[string]interface{}{"Age": 30, "Name": "Tommy"}) + + sql, vars := clause.Build(UPDATE, SET, WHERE) + t.Log(sql, vars) + if sql != "UPDATE User SET Age = ?, Name = ? WHERE Name = ?" { + t.Fatal("failed to build SQL") + } + if !reflect.DeepEqual(vars, []interface{}{30, "Tommy", "Tom"}) { + t.Fatal("failed to build SQLVars") + } +} + +func testDelete(t *testing.T) { + var clause Clause + clause.Set(DELETE, "User") + clause.Set(WHERE, "Name = ?", "Tom") + + sql, vars := clause.Build(DELETE, WHERE) + t.Log(sql, vars) + if sql != "DELETE FROM User WHERE Name = ?" { + t.Fatal("failed to build SQL") + } + if !reflect.DeepEqual(vars, []interface{}{"Tom"}) { + t.Fatal("failed to build SQLVars") + } +} + +func TestClause_Build(t *testing.T) { + t.Run("select", func(t *testing.T) { + testSelect(t) + }) + t.Run("update", func(t *testing.T) { + testUpdate(t) + }) + t.Run("delete", func(t *testing.T) { + testDelete(t) + }) +} diff --git a/gee-orm/day6-transaction/clause/generator.go b/gee-orm/day6-transaction/clause/generator.go new file mode 100644 index 0000000..1cf5aec --- /dev/null +++ b/gee-orm/day6-transaction/clause/generator.go @@ -0,0 +1,106 @@ +package clause + +import ( + "fmt" + "strings" +) + +type generator func(values ...interface{}) (string, []interface{}) + +var generators map[Type]generator + +func init() { + generators = make(map[Type]generator) + generators[INSERT] = _insert + generators[VALUES] = _values + generators[SELECT] = _select + generators[LIMIT] = _limit + generators[WHERE] = _where + generators[ORDERBY] = _orderby + generators[UPDATE] = _update + generators[SET] = _set + generators[DELETE] = _delete + generators[COUNT] = _count +} + +func genBindVars(num int) string { + var vars []string + for i := 0; i < num; i++ { + vars = append(vars, "?") + } + return strings.Join(vars, ", ") +} + +func _insert(values ...interface{}) (string, []interface{}) { + // INSERT INTO $tableName ($fields) + tableName := values[0] + fields := strings.Join(values[1].([]string), ",") + return fmt.Sprintf("INSERT INTO %s (%v)", tableName, fields), []interface{}{} +} + +func _values(values ...interface{}) (string, []interface{}) { + // VALUES ($v1), (&v2), ... + var bindStr string + var sql strings.Builder + var vars []interface{} + sql.WriteString("VALUES ") + for i, value := range values { + v := value.([]interface{}) + if bindStr == "" { + bindStr = genBindVars(len(v)) + } + sql.WriteString(fmt.Sprintf("(%v)", bindStr)) + if i+1 != len(values) { + sql.WriteString(", ") + } + vars = append(vars, v...) + } + return sql.String(), vars + +} + +func _select(values ...interface{}) (string, []interface{}) { + // SELECT $fields FROM $tableName + tableName := values[0] + fields := strings.Join(values[1].([]string), ",") + return fmt.Sprintf("SELECT %v FROM %s", fields, tableName), []interface{}{} +} + +func _limit(values ...interface{}) (string, []interface{}) { + // LIMIT $num + return "LIMIT ?", values +} + +func _where(values ...interface{}) (string, []interface{}) { + // WHERE $desc + desc, vars := values[0], values[1:] + return fmt.Sprintf("WHERE %s", desc), vars +} + +func _orderby(values ...interface{}) (string, []interface{}) { + return fmt.Sprintf("ORDER BY %s", values[0]), []interface{}{} +} + +func _update(values ...interface{}) (string, []interface{}) { + return fmt.Sprintf("UPDATE %s", values[0]), []interface{}{} +} + +func _set(values ...interface{}) (string, []interface{}) { + m := values[0].(map[string]interface{}) + var keys []string + var vars []interface{} + for k, v := range m { + keys = append(keys, k+" = ?") + vars = append(vars, v) + } + + return fmt.Sprintf("SET %s", strings.Join(keys, ", ")), vars +} + +func _delete(values ...interface{}) (string, []interface{}) { + return fmt.Sprintf("DELETE FROM %s", values[0]), []interface{}{} +} + +func _count(values ...interface{}) (string, []interface{}) { + return _select(values[0], []string{"count(*)"}) +} diff --git a/gee-orm/day6-transaction/dialect/dialect.go b/gee-orm/day6-transaction/dialect/dialect.go new file mode 100644 index 0000000..4696314 --- /dev/null +++ b/gee-orm/day6-transaction/dialect/dialect.go @@ -0,0 +1,22 @@ +package dialect + +import "reflect" + +var dialectsMap = map[string]Dialect{} + +// Dialect is an interface contains methods that a dialect has to implement +type Dialect interface { + DataTypeOf(typ reflect.Value) string + TableExistSQL(tableName string) (string, []interface{}) +} + +// RegisterDialect register a dialect to the global variable +func RegisterDialect(name string, dialect Dialect) { + dialectsMap[name] = dialect +} + +// Get the dialect from global variable if it exists +func GetDialect(name string) (dialect Dialect, ok bool) { + dialect, ok = dialectsMap[name] + return +} diff --git a/gee-orm/day6-transaction/dialect/sqlite3.go b/gee-orm/day6-transaction/dialect/sqlite3.go new file mode 100644 index 0000000..f3c3897 --- /dev/null +++ b/gee-orm/day6-transaction/dialect/sqlite3.go @@ -0,0 +1,45 @@ +package dialect + +import ( + "fmt" + "reflect" + "time" +) + +type sqlite3 struct{} + +var _ Dialect = (*sqlite3)(nil) + +func init() { + RegisterDialect("sqlite3", &sqlite3{}) +} + +// Get Data Type for sqlite3 Dialect +func (s *sqlite3) DataTypeOf(typ reflect.Value) string { + switch typ.Kind() { + case reflect.Bool: + return "bool" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: + return "integer" + case reflect.Int64, reflect.Uint64: + return "bigint" + case reflect.Float32, reflect.Float64: + return "real" + case reflect.String: + return "text" + case reflect.Array, reflect.Slice: + return "blob" + case reflect.Struct: + if _, ok := typ.Interface().(time.Time); ok { + return "datetime" + } + } + panic(fmt.Sprintf("invalid sql type %s (%s)", typ.Type().Name(), typ.Kind())) +} + +// TableExistSQL returns SQL that judge whether the table exists in database +func (s *sqlite3) TableExistSQL(tableName string) (string, []interface{}) { + args := []interface{}{tableName} + return "SELECT name FROM sqlite_master WHERE type='table' and name = ?", args +} diff --git a/gee-orm/day6-transaction/dialect/sqlite3_test.go b/gee-orm/day6-transaction/dialect/sqlite3_test.go new file mode 100644 index 0000000..3df5f07 --- /dev/null +++ b/gee-orm/day6-transaction/dialect/sqlite3_test.go @@ -0,0 +1,25 @@ +package dialect + +import ( + "reflect" + "testing" +) + +func TestDataTypeOf(t *testing.T) { + dial := &sqlite3{} + cases := []struct { + Value interface{} + Type string + }{ + {"Tom", "text"}, + {123, "integer"}, + {1.2, "real"}, + {[]int{1, 2, 3}, "blob"}, + } + + for _, c := range cases { + if typ := dial.DataTypeOf(reflect.ValueOf(c.Value)); typ != c.Type { + t.Fatalf("expect %s, but got %s", c.Type, typ) + } + } +} diff --git a/gee-orm/day6-transaction/geeorm.go b/gee-orm/day6-transaction/geeorm.go new file mode 100644 index 0000000..b8cbb42 --- /dev/null +++ b/gee-orm/day6-transaction/geeorm.go @@ -0,0 +1,75 @@ +package geeorm + +import ( + "database/sql" + "geeorm/dialect" + "geeorm/log" + "geeorm/session" +) + +// Engine is the main struct of geeorm, manages all db sessions and transactions. +type Engine struct { + db *sql.DB + dialect dialect.Dialect +} + +// NewEngine create a instance of Engine +// connect database and ping it to test whether it's alive +func NewEngine(driver, source string) (e *Engine, err error) { + db, err := sql.Open(driver, source) + if err != nil { + log.Error(err) + return + } + // Send a ping to make sure the database connection is alive. + if err = db.Ping(); err != nil { + log.Error(err) + return + } + // make sure the specific dialect exists + dial, ok := dialect.GetDialect(driver) + if !ok { + log.Errorf("dialect %s Not Found", driver) + return + } + e = &Engine{db: db, dialect: dial} + log.Info("Connect database success") + return +} + +// Close database connection +func (e *Engine) Close() (err error) { + if err = e.db.Close(); err == nil { + log.Info("Close database success") + } + return +} + +// NewSession creates a new session for next operations +func (e *Engine) NewSession() *session.Session { + return session.New(e.db, e.dialect) +} + +// TxFunc will be called between tx.Begin() and tx.Commit() +// https://stackoverflow.com/questions/16184238/database-sql-tx-detecting-commit-or-rollback +type TxFunc func(*session.Session) (interface{}, error) + +// Transaction executes sql wrapped in a transaction, then automatically commit if no error occurs +func (e *Engine) Transaction(f TxFunc) (result interface{}, err error) { + s := e.NewSession() + if err := s.Begin(); err != nil { + return nil, err + } + defer func() { + if p := recover(); p != nil { + _ = s.Rollback() + panic(p) // re-throw panic after Rollback + } else if err != nil { + _ = s.Rollback() // err is non-nil; don't change it + } else { + err = s.Commit() // err is nil; if Commit returns error update err + } + }() + + return f(s) +} diff --git a/gee-orm/day6-transaction/geeorm_test.go b/gee-orm/day6-transaction/geeorm_test.go new file mode 100644 index 0000000..7b15616 --- /dev/null +++ b/gee-orm/day6-transaction/geeorm_test.go @@ -0,0 +1,71 @@ +package geeorm + +import ( + "errors" + "geeorm/session" + "testing" + + _ "github.com/mattn/go-sqlite3" +) + +func OpenDB(t *testing.T) *Engine { + t.Helper() + engine, err := NewEngine("sqlite3", "gee.db") + if err != nil { + t.Fatal("failed to connect", err) + } + return engine +} + +func CloseDB(engine *Engine) { + _ = engine.Close() +} + +func TestNewEngine(t *testing.T) { + engine := OpenDB(t) + _ = engine.Close() +} + +type User struct { + Name string `geeorm:"primary_key"` + Age int +} + +func transactionRollback(t *testing.T) { + engine := OpenDB(t) + defer CloseDB(engine) + _ = engine.NewSession().DropTable(&User{}) + _, err := engine.Transaction(func(s *session.Session) (result interface{}, err error) { + _ = s.CreateTable(&User{}) + _, err = s.Create(&User{"Tom", 18}) + return nil, errors.New("Error") + }) + if err == nil || engine.NewSession().HasTable("User") { + t.Fatal("failed to rollback") + } +} + +func transactionCommit(t *testing.T) { + engine := OpenDB(t) + defer CloseDB(engine) + _ = engine.NewSession().DropTable(&User{}) + _, err := engine.Transaction(func(s *session.Session) (result interface{}, err error) { + err = s.CreateTable(&User{}) + _, err = s.Create(&User{"Tom", 18}) + return + }) + u := &User{} + _ = engine.NewSession().First(u) + if err != nil || u.Name != "Tom" { + t.Fatal("failed to commit") + } +} + +func TestEngine_Transaction(t *testing.T) { + t.Run("rollback", func(t *testing.T) { + transactionRollback(t) + }) + t.Run("commit", func(t *testing.T) { + transactionCommit(t) + }) +} diff --git a/gee-orm/day6-transaction/go.mod b/gee-orm/day6-transaction/go.mod new file mode 100644 index 0000000..043b1c6 --- /dev/null +++ b/gee-orm/day6-transaction/go.mod @@ -0,0 +1,5 @@ +module geeorm + +go 1.13 + +require github.com/mattn/go-sqlite3 v2.0.3+incompatible diff --git a/gee-orm/day6-transaction/log/log.go b/gee-orm/day6-transaction/log/log.go new file mode 100644 index 0000000..eacc0c6 --- /dev/null +++ b/gee-orm/day6-transaction/log/log.go @@ -0,0 +1,47 @@ +package log + +import ( + "io/ioutil" + "log" + "os" + "sync" +) + +var ( + errorLog = log.New(os.Stdout, "\033[31m[error]\033[0m ", log.LstdFlags|log.Lshortfile) + infoLog = log.New(os.Stdout, "\033[34m[info ]\033[0m ", log.LstdFlags|log.Lshortfile) + loggers = []*log.Logger{errorLog, infoLog} + mu sync.Mutex +) + +// log methods +var ( + Error = errorLog.Println + Errorf = errorLog.Printf + Info = infoLog.Println + Infof = infoLog.Printf +) + +// log levels +const ( + InfoLevel = iota + ErrorLevel + Disabled +) + +// SetLevel controls log level +func SetLevel(level int) { + mu.Lock() + defer mu.Unlock() + + for _, logger := range loggers { + logger.SetOutput(os.Stdout) + } + + if ErrorLevel < level { + errorLog.SetOutput(ioutil.Discard) + } + if InfoLevel < level { + infoLog.SetOutput(ioutil.Discard) + } +} diff --git a/gee-orm/day6-transaction/log/log_test.go b/gee-orm/day6-transaction/log/log_test.go new file mode 100644 index 0000000..8cd403c --- /dev/null +++ b/gee-orm/day6-transaction/log/log_test.go @@ -0,0 +1,17 @@ +package log + +import ( + "os" + "testing" +) + +func TestSetLevel(t *testing.T) { + SetLevel(ErrorLevel) + if infoLog.Writer() == os.Stdout || errorLog.Writer() != os.Stdout { + t.Fatal("failed to set log level") + } + SetLevel(Disabled) + if infoLog.Writer() == os.Stdout || errorLog.Writer() == os.Stdout { + t.Fatal("failed to set log level") + } +} \ No newline at end of file diff --git a/gee-orm/day6-transaction/schema/schema.go b/gee-orm/day6-transaction/schema/schema.go new file mode 100644 index 0000000..8519fd4 --- /dev/null +++ b/gee-orm/day6-transaction/schema/schema.go @@ -0,0 +1,67 @@ +package schema + +import ( + "fmt" + "geeorm/dialect" + "go/ast" + "reflect" +) + +// Field represents a column of database +type Field struct { + Name string + Tag string +} + +// Schema represents a table of database +type Schema struct { + TableName string + PrimaryField *Field + Fields []*Field + FieldNames []string +} + +// Values return the values of dest's member variables +func (schema *Schema) Values(dest interface{}) []interface{} { + destValue := reflect.Indirect(reflect.ValueOf(dest)) + var fieldValues []interface{} + for _, field := range schema.Fields { + fieldValues = append(fieldValues, destValue.FieldByName(field.Name).Interface()) + } + return fieldValues +} + +// Parse a struct to a Schema instance +func Parse(dest interface{}, d dialect.Dialect) *Schema { + modelType := reflect.Indirect(reflect.ValueOf(dest)).Type() + schema := &Schema{ + TableName: modelType.Name(), + PrimaryField: &Field{Name: "ID", Tag: ""}, + } + + for i := 0; i < modelType.NumField(); i++ { + p := modelType.Field(i) + if !p.Anonymous && ast.IsExported(p.Name) { + field := &Field{ + Name: p.Name, + Tag: d.DataTypeOf(reflect.Indirect(reflect.New(p.Type))), + } + if v, ok := p.Tag.Lookup("geeorm"); ok && v == "primary_key" { + schema.PrimaryField = field + } + schema.Fields = append(schema.Fields, field) + schema.FieldNames = append(schema.FieldNames, p.Name) + } + } + return schema +} + +// String returns readable string +func (field *Field) String() string { + return fmt.Sprintf("(%s %s)", field.Name, field.Tag) +} + +// String returns readable string +func (schema *Schema) String() string { + return fmt.Sprintf("TABLE %s %v", schema.TableName, schema.Fields) +} diff --git a/gee-orm/day6-transaction/schema/schema_test.go b/gee-orm/day6-transaction/schema/schema_test.go new file mode 100644 index 0000000..aba3e0b --- /dev/null +++ b/gee-orm/day6-transaction/schema/schema_test.go @@ -0,0 +1,36 @@ +package schema + +import ( + "geeorm/dialect" + "testing" +) + +type User struct { + Name string `geeorm:"primary_key"` + Age int +} + +var TestDial, _ = dialect.GetDialect("sqlite3") + +func TestParse(t *testing.T) { + schema := Parse(&User{}, TestDial) + if schema.TableName != "User" || len(schema.Fields) != 2 { + t.Fatal("failed to parse User struct") + } + if schema.PrimaryField.Name != "Name" { + t.Fatal("failed to parse primary key") + } + t.Log(schema) +} + +func TestSchema_Values(t *testing.T) { + schema := Parse(&User{}, TestDial) + values := schema.Values(&User{"Tom", 18}) + + name := values[0].(string) + age := values[1].(int) + + if name != "Tom" || age != 18 { + t.Fatal("failed to get values") + } +} diff --git a/gee-orm/day6-transaction/session/raw.go b/gee-orm/day6-transaction/session/raw.go new file mode 100644 index 0000000..d99f631 --- /dev/null +++ b/gee-orm/day6-transaction/session/raw.go @@ -0,0 +1,88 @@ +package session + +import ( + "database/sql" + "geeorm/clause" + "geeorm/dialect" + "geeorm/log" + "geeorm/schema" +) + +// Session keep a pointer to sql.DB and provides all execution of all +// kind of database operations. +type Session struct { + db *sql.DB + dialect dialect.Dialect + tx *sql.Tx + refTable *schema.Schema + clause clause.Clause + sql string + sqlVars []interface{} +} + +// New creates a instance of Session +func New(db *sql.DB, dialect dialect.Dialect) *Session { + return &Session{ + db: db, + dialect: dialect, + } +} + +// Clear initialize the state of a session, except for isAutoCommit +func (s *Session) Clear() { + s.refTable, s.sqlVars = nil, nil + s.clause = clause.Clause{} + s.sql = "" +} + +// CommonDB is a minimal function set of db +type CommonDB interface { + Query(query string, args ...interface{}) (*sql.Rows, error) + QueryRow(query string, args ...interface{}) *sql.Row + Exec(query string, args ...interface{}) (sql.Result, error) +} + +var _ CommonDB = (*sql.DB)(nil) +var _ CommonDB = (*sql.Tx)(nil) + +// DB returns tx if a tx begins. otherwise return *sql.DB +func (s *Session) DB() CommonDB { + if s.tx != nil { + return s.tx + } + return s.db +} + +// Exec raw sql with sqlVars +func (s *Session) Exec() (result sql.Result, err error) { + defer s.Clear() + log.Info(s.sql, s.sqlVars) + if result, err = s.DB().Exec(s.sql, s.sqlVars...); err != nil { + log.Error(err) + } + return +} + +// QueryRow gets a record from db +func (s *Session) QueryRow() *sql.Row { + defer s.Clear() + log.Info(s.sql, s.sqlVars) + return s.DB().QueryRow(s.sql, s.sqlVars...) +} + +// QueryRows gets a list of records from db +func (s *Session) QueryRows() (rows *sql.Rows, err error) { + defer s.Clear() + log.Info(s.sql, s.sqlVars) + if rows, err = s.DB().Query(s.sql, s.sqlVars...); err != nil { + log.Error(err) + } + return +} + +// Raw appends sql and sqlVars +func (s *Session) Raw(sql string, values ...interface{}) *Session { + s.sql += sql + s.sqlVars = append(s.sqlVars, values...) + return s +} diff --git a/gee-orm/day6-transaction/session/raw_test.go b/gee-orm/day6-transaction/session/raw_test.go new file mode 100644 index 0000000..ce1e75b --- /dev/null +++ b/gee-orm/day6-transaction/session/raw_test.go @@ -0,0 +1,46 @@ +package session + +import ( + "database/sql" + "os" + "testing" + + "geeorm/dialect" + + _ "github.com/mattn/go-sqlite3" +) + +var ( + TestDB *sql.DB + TestDial, _ = dialect.GetDialect("sqlite3") +) + +func TestMain(m *testing.M) { + TestDB, _ = sql.Open("sqlite3", "gee.db") + code := m.Run() + _ = TestDB.Close() + os.Exit(code) +} + +func NewSession() *Session { + return &Session{db: TestDB, dialect: TestDial} +} + +func TestSession_Exec(t *testing.T) { + _, _ = NewSession().Raw("DROP TABLE IF EXISTS User;").Exec() + _, _ = NewSession().Raw("CREATE TABLE User(name text);").Exec() + result, _ := NewSession().Raw("INSERT INTO User(`name`) values (?), (?)", "Tom", "Sam").Exec() + if count, err := result.RowsAffected(); err != nil || count != 2 { + t.Fatal("expect 2, but got", count) + } +} + +func TestSession_QueryRows(t *testing.T) { + _, _ = NewSession().Raw("DROP TABLE IF EXISTS User;").Exec() + _, _ = NewSession().Raw("CREATE TABLE User(name text);").Exec() + row := NewSession().Raw("SELECT count(*) FROM User").QueryRow() + var count int + if err := row.Scan(&count); err != nil || count != 0 { + t.Fatal("failed to query db", err) + } +} diff --git a/gee-orm/day6-transaction/session/record.go b/gee-orm/day6-transaction/session/record.go new file mode 100644 index 0000000..bc6d1fe --- /dev/null +++ b/gee-orm/day6-transaction/session/record.go @@ -0,0 +1,130 @@ +package session + +import ( + "geeorm/clause" + "reflect" +) + +// Create one or more records in database +func (s *Session) Create(values ...interface{}) (int64, error) { + recordValues := make([]interface{}, 0) + for _, value := range values { + table := s.RefTable(value) + s.clause.Set(clause.INSERT, table.TableName, table.FieldNames) + recordValues = append(recordValues, table.Values(value)) + } + + s.clause.Set(clause.VALUES, recordValues...) + sql, vars := s.clause.Build(clause.INSERT, clause.VALUES) + result, err := s.Raw(sql, vars...).Exec() + if err != nil { + return 0, err + } + + return result.RowsAffected() +} + +// Find gets all eligible records +func (s *Session) Find(values interface{}) error { + destSlice := reflect.Indirect(reflect.ValueOf(values)) + destType := destSlice.Type().Elem() + table := s.RefTable(reflect.New(destType).Elem().Interface()) + + s.clause.Set(clause.SELECT, table.TableName, table.FieldNames) + sql, vars := s.clause.Build(clause.SELECT, clause.WHERE, clause.ORDERBY, clause.LIMIT) + rows, err := s.Raw(sql, vars...).QueryRows() + if err != nil { + return err + } + + for rows.Next() { + dest := reflect.New(destType).Elem() + var values []interface{} + for _, name := range table.FieldNames { + values = append(values, dest.FieldByName(name).Addr().Interface()) + } + if err := rows.Scan(values...); err != nil { + return err + } + destSlice.Set(reflect.Append(destSlice, dest)) + } + return rows.Close() +} + +// First gets the 1st row +func (s *Session) First(value interface{}) error { + dest := reflect.Indirect(reflect.ValueOf(value)) + destSlice := reflect.New(reflect.SliceOf(dest.Type())).Elem() + err := s.Limit(1).Find(destSlice.Addr().Interface()) + dest.Set(destSlice.Index(0)) + return err +} + +// Limit adds limit condition to clause +func (s *Session) Limit(num int) *Session { + s.clause.Set(clause.LIMIT, num) + return s +} + +// Where adds limit condition to clause +func (s *Session) Where(desc string, args ...interface{}) *Session { + var vars []interface{} + s.clause.Set(clause.WHERE, append(append(vars, desc), args...)...) + return s +} + +// OrderBy adds order by condition to clause +func (s *Session) OrderBy(desc string) *Session { + s.clause.Set(clause.ORDERBY, desc) + return s +} + +// Set adds Assignment by condition to clause +// support map[string]interface{} +// also support "Name", "Tom", "Age", 18, etc +func (s *Session) Set(values ...interface{}) *Session { + m, ok := values[0].(map[string]interface{}) + if !ok { + m = make(map[string]interface{}) + for i := 0; i < len(values); i += 2 { + m[values[i].(string)] = values[i+1] + } + } + s.clause.Set(clause.SET, m) + return s +} + +// Update records with where clause +func (s *Session) Update(value interface{}) (int64, error) { + s.clause.Set(clause.UPDATE, s.guessTableName(value)) + sql, vars := s.clause.Build(clause.UPDATE, clause.SET, clause.WHERE) + result, err := s.Raw(sql, vars...).Exec() + if err != nil { + return 0, err + } + return result.RowsAffected() +} + +// Delete records with where clause +func (s *Session) Delete(value interface{}) (int64, error) { + s.clause.Set(clause.DELETE, s.guessTableName(value)) + sql, vars := s.clause.Build(clause.DELETE, clause.WHERE) + result, err := s.Raw(sql, vars...).Exec() + if err != nil { + return 0, err + } + return result.RowsAffected() +} + +// Count records with where clause +func (s *Session) Count(value interface{}) (int64, error) { + s.clause.Set(clause.COUNT, s.guessTableName(value)) + sql, vars := s.clause.Build(clause.COUNT, clause.WHERE) + row := s.Raw(sql, vars...).QueryRow() + var tmp int64 + + if err := row.Scan(&tmp); err != nil { + return 0, err + } + return tmp, nil +} diff --git a/gee-orm/day6-transaction/session/record_test.go b/gee-orm/day6-transaction/session/record_test.go new file mode 100644 index 0000000..3489287 --- /dev/null +++ b/gee-orm/day6-transaction/session/record_test.go @@ -0,0 +1,96 @@ +package session + +import "testing" + +var ( + user1 = &User{"Tom", 18} + user2 = &User{"Sam", 25} + user3 = &User{"Jack", 25} +) + +func testRecordInit(t *testing.T) { + t.Helper() + err1 := NewSession().DropTable(&User{}) + err2 := NewSession().CreateTable(&User{}) + _, err3 := NewSession().Create(user1, user2) + if err1 != nil || err2 != nil || err3 != nil { + t.Fatal("failed init test records") + } + +} + +func TestSession_Create(t *testing.T) { + testRecordInit(t) + affected, err := NewSession().Create(user3) + if err != nil || affected != 1 { + t.Fatal("failed to create record") + } +} + +func TestSession_Find(t *testing.T) { + testRecordInit(t) + users := []User{} + if err := NewSession().Find(&users); err != nil || len(users) != 2 { + t.Fatal("failed to query all") + } +} + +func TestSession_First(t *testing.T) { + testRecordInit(t) + u := &User{} + err := NewSession().First(u) + if err != nil || u.Name != "Tom" || u.Age != 18 { + t.Fatal("failed to query first") + } +} + +func TestSession_Limit(t *testing.T) { + testRecordInit(t) + var users []User + err := NewSession().Limit(1).Find(&users) + if err != nil || len(users) != 1 { + t.Fatal("failed to query with limit condition") + } +} + +func TestSession_Where(t *testing.T) { + testRecordInit(t) + var users []User + _, err1 := NewSession().Create(user3) + err2 := NewSession().Where("Age = ?", 25).Find(&users) + + if err1 != nil || err2 != nil || len(users) != 2 { + t.Fatal("failed to query with where condition") + } +} + +func TestSession_OrderBy(t *testing.T) { + testRecordInit(t) + u := &User{} + err := NewSession().OrderBy("Age DESC").First(u) + + if err != nil || u.Age != 25 { + t.Fatal("failed to query with order by condition") + } +} + +func TestSession_Update(t *testing.T) { + testRecordInit(t) + affected, _ := NewSession().Where("Name = ?", "Tom").Set("Age", 30).Update(&User{}) + u := &User{} + _ = NewSession().OrderBy("Age DESC").First(u) + + if affected != 1 || u.Age != 30 { + t.Fatal("failed to update") + } +} + +func TestSession_DeleteAndCount(t *testing.T) { + testRecordInit(t) + affected, _ := NewSession().Where("Name = ?", "Tom").Delete("User") + count, _ := NewSession().Count("User") + + if affected != 1 || count != 1 { + t.Fatal("failed to delete or count") + } +} diff --git a/gee-orm/day6-transaction/session/table.go b/gee-orm/day6-transaction/session/table.go new file mode 100644 index 0000000..b644d3a --- /dev/null +++ b/gee-orm/day6-transaction/session/table.go @@ -0,0 +1,59 @@ +package session + +import ( + "fmt" + "strings" + + "geeorm/schema" +) + +// RefTable returns a Schema instance that contains all parsed fields +func (s *Session) RefTable(value interface{}) *schema.Schema { + if value == nil { + panic("value is nil") + } + if s.refTable == nil { + s.refTable = schema.Parse(value, s.dialect) + } + return s.refTable +} + +// CreateTable create a table in database with a model +func (s *Session) CreateTable(value interface{}) error { + table := s.RefTable(value) + var columns []string + for _, field := range table.Fields { + tag := field.Tag + if field.Name == table.PrimaryField.Name { + tag += " PRIMARY KEY" + } + columns = append(columns, fmt.Sprintf("%s %s", field.Name, tag)) + } + desc := strings.Join(columns, ",") + _, err := s.Raw(fmt.Sprintf("CREATE TABLE %s (%s);", table.TableName, desc)).Exec() + return err +} + +// DropTable drops a table with the name of model +func (s *Session) DropTable(value interface{}) error { + table := s.RefTable(value) + _, err := s.Raw(fmt.Sprintf("DROP TABLE IF EXISTS %s", table.TableName)).Exec() + return err +} + +func (s *Session) guessTableName(value interface{}) string { + if tableName, ok := value.(string); ok { + return tableName + } + return s.RefTable(value).TableName +} + +// HasTable returns true of the table exists +func (s *Session) HasTable(value interface{}) bool { + tableName := s.guessTableName(value) + sql, values := s.dialect.TableExistSQL(tableName) + row := s.Raw(sql, values...).QueryRow() + var tmp string + _ = row.Scan(&tmp) + return tmp == tableName +} diff --git a/gee-orm/day6-transaction/session/table_test.go b/gee-orm/day6-transaction/session/table_test.go new file mode 100644 index 0000000..5c934fa --- /dev/null +++ b/gee-orm/day6-transaction/session/table_test.go @@ -0,0 +1,18 @@ +package session + +import ( + "testing" +) + +type User struct { + Name string `geeorm:"primary_key"` + Age int +} + +func TestSession_CreateTable(t *testing.T) { + _ = NewSession().DropTable(&User{}) + _ = NewSession().CreateTable(&User{}) + if !NewSession().HasTable("User") { + t.Fatal("failed to create table User") + } +} diff --git a/gee-orm/day6-transaction/session/transaction.go b/gee-orm/day6-transaction/session/transaction.go new file mode 100644 index 0000000..3cdb451 --- /dev/null +++ b/gee-orm/day6-transaction/session/transaction.go @@ -0,0 +1,31 @@ +package session + +import "geeorm/log" + +// Begin a transaction +func (s *Session) Begin() (err error) { + log.Info("transaction begin") + if s.tx, err = s.db.Begin(); err != nil { + log.Error(err) + return + } + return +} + +// Commit a transaction +func (s *Session) Commit() (err error) { + log.Info("transaction commit") + if err = s.tx.Commit(); err != nil { + log.Error(err) + } + return +} + +// Rollback a transaction +func (s *Session) Rollback() (err error) { + log.Info("transaction rollback") + if err = s.tx.Rollback(); err != nil { + log.Error(err) + } + return +}