diff --git a/gee-orm/day3-save-query/geeorm/clause/clause.go b/gee-orm/day3-save-query/geeorm/clause/clause.go index 8ee7b16..77d88bc 100755 --- a/gee-orm/day3-save-query/geeorm/clause/clause.go +++ b/gee-orm/day3-save-query/geeorm/clause/clause.go @@ -15,10 +15,12 @@ type Type int // Support types for Clause const ( - INSERT Type = 0 - VALUES Type = 1 - SELECT Type = 2 - LIMIT Type = 3 + INSERT Type = 0 + VALUES Type = 1 + SELECT Type = 2 + LIMIT Type = 3 + WHERE Type = 4 + ORDERBY Type = 5 ) // Set adds a sub clause of specific type @@ -33,7 +35,7 @@ func (c *Clause) Set(name Type, vars ...interface{}) { } // Build generate the final SQL and SQLVars -func (c *Clause) Build(orders []Type) (string, []interface{}) { +func (c *Clause) Build(orders ...Type) (string, []interface{}) { var sqls []string var vars []interface{} for _, order := range orders { diff --git a/gee-orm/day3-save-query/geeorm/clause/clause_test.go b/gee-orm/day3-save-query/geeorm/clause/clause_test.go index 0f3db6d..3062bbd 100755 --- a/gee-orm/day3-save-query/geeorm/clause/clause_test.go +++ b/gee-orm/day3-save-query/geeorm/clause/clause_test.go @@ -20,13 +20,14 @@ func TestClause_Build(t *testing.T) { var clause Clause clause.Set(LIMIT, 3) clause.Set(SELECT, "User", []string{"*"}) - orders := []Type{SELECT, LIMIT} - sql, vars := clause.Build(orders) + clause.Set(WHERE, "Name = ?", 18) + clause.Set(ORDERBY, "Age ASC") + sql, vars := clause.Build(SELECT, WHERE, ORDERBY, LIMIT) t.Log(sql, vars) - if sql != "SELECT * FROM User LIMIT ?" { + if sql != "SELECT * FROM User WHERE Name = ? ORDER BY Age ASC LIMIT ?" { t.Fatal("failed to build SQL") } - if !reflect.DeepEqual(vars, []interface{}{3}) { + if !reflect.DeepEqual(vars, []interface{}{18, 3}) { t.Fatal("failed to build SQLVars") } } diff --git a/gee-orm/day3-save-query/geeorm/clause/generator.go b/gee-orm/day3-save-query/geeorm/clause/generator.go index 78608d6..0ac29ee 100755 --- a/gee-orm/day3-save-query/geeorm/clause/generator.go +++ b/gee-orm/day3-save-query/geeorm/clause/generator.go @@ -15,6 +15,8 @@ func init() { generators[VALUES] = _values generators[SELECT] = _select generators[LIMIT] = _limit + generators[WHERE] = _where + generators[ORDERBY] = _orderby } func genBindVars(num int) string { @@ -64,3 +66,13 @@ 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{}{} +} diff --git a/gee-orm/day3-save-query/geeorm/session/record.go b/gee-orm/day3-save-query/geeorm/session/record.go index cf35552..1a0672a 100644 --- a/gee-orm/day3-save-query/geeorm/session/record.go +++ b/gee-orm/day3-save-query/geeorm/session/record.go @@ -15,7 +15,7 @@ func (s *Session) Create(values ...interface{}) (int64, error) { } s.clause.Set(clause.VALUES, recordValues...) - sql, vars := s.clause.Build([]clause.Type{clause.INSERT, clause.VALUES}) + sql, vars := s.clause.Build(clause.INSERT, clause.VALUES) result, err := s.Raw(sql, vars...).Exec() if err != nil { return 0, err @@ -24,25 +24,6 @@ func (s *Session) Create(values ...interface{}) (int64, error) { return result.RowsAffected() } -// First gets the 1st row -func (s *Session) First(value interface{}) error { - table := s.RefTable(value) - - s.clause.Set(clause.SELECT, table.TableName, table.FieldNames) - s.clause.Set(clause.LIMIT, 1) - - sql, vars := s.clause.Build([]clause.Type{clause.SELECT, clause.LIMIT}) - row := s.Raw(sql, vars...).QueryRow() - - dest := reflect.ValueOf(value).Elem() - var values []interface{} - for _, name := range table.FieldNames { - values = append(values, dest.FieldByName(name).Addr().Interface()) - } - - return row.Scan(values...) -} - // Find gets all eligible records func (s *Session) Find(values interface{}) error { destSlice := reflect.Indirect(reflect.ValueOf(values)) @@ -50,7 +31,7 @@ func (s *Session) Find(values interface{}) error { table := s.RefTable(reflect.New(destType).Elem().Interface()) s.clause.Set(clause.SELECT, table.TableName, table.FieldNames) - sql, vars := s.clause.Build([]clause.Type{clause.SELECT}) + 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 diff --git a/gee-orm/day3-save-query/geeorm/session/record_test.go b/gee-orm/day3-save-query/geeorm/session/record_test.go index f57680e..27b6d77 100644 --- a/gee-orm/day3-save-query/geeorm/session/record_test.go +++ b/gee-orm/day3-save-query/geeorm/session/record_test.go @@ -5,31 +5,30 @@ 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) { - _ = NewSession().DropTable(&User{}) - _ = NewSession().CreateTable(&User{}) - if affected, err := NewSession().Create(user1, user2); err != nil || affected != 2 { + testRecordInit(t) + affected, err := NewSession().Create(user3) + if err != nil || affected != 1 { t.Fatal("failed to create record") } } -func TestSession_First(t *testing.T) { - _ = NewSession().DropTable(&User{}) - _ = NewSession().CreateTable(&User{}) - _, _ = NewSession().Create(user1) - u := &User{} - err := NewSession().First(u) - if err != nil || u.Age != user1.Age || u.Name != user1.Name { - t.Fatal("failed to query first") - } -} - func TestSession_Find(t *testing.T) { - _ = NewSession().DropTable(&User{}) - _ = NewSession().CreateTable(&User{}) - _, _ = NewSession().Create(user1, user2) + testRecordInit(t) users := []User{} if err := NewSession().Find(&users); err != nil || len(users) != 2 { t.Fatal("failed to query all") diff --git a/gee-orm/day4-chain-operation/geeorm/clause/clause.go b/gee-orm/day4-chain-operation/geeorm/clause/clause.go new file mode 100755 index 0000000..77d88bc --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/clause/clause.go @@ -0,0 +1,48 @@ +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 = 0 + VALUES Type = 1 + SELECT Type = 2 + LIMIT Type = 3 + WHERE Type = 4 + ORDERBY Type = 5 +) + +// 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/day4-chain-operation/geeorm/clause/clause_test.go b/gee-orm/day4-chain-operation/geeorm/clause/clause_test.go new file mode 100755 index 0000000..3062bbd --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/clause/clause_test.go @@ -0,0 +1,33 @@ +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 TestClause_Build(t *testing.T) { + var clause Clause + clause.Set(LIMIT, 3) + clause.Set(SELECT, "User", []string{"*"}) + clause.Set(WHERE, "Name = ?", 18) + 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{}{18, 3}) { + t.Fatal("failed to build SQLVars") + } +} diff --git a/gee-orm/day4-chain-operation/geeorm/clause/generator.go b/gee-orm/day4-chain-operation/geeorm/clause/generator.go new file mode 100755 index 0000000..0ac29ee --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/clause/generator.go @@ -0,0 +1,78 @@ +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 +} + +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{}{} +} diff --git a/gee-orm/day4-chain-operation/geeorm/dialect/dialect.go b/gee-orm/day4-chain-operation/geeorm/dialect/dialect.go new file mode 100644 index 0000000..4696314 --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/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/day4-chain-operation/geeorm/dialect/sqlite3.go b/gee-orm/day4-chain-operation/geeorm/dialect/sqlite3.go new file mode 100644 index 0000000..f3c3897 --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/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/day4-chain-operation/geeorm/dialect/sqlite3_test.go b/gee-orm/day4-chain-operation/geeorm/dialect/sqlite3_test.go new file mode 100644 index 0000000..3df5f07 --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/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/day4-chain-operation/geeorm/geeorm.go b/gee-orm/day4-chain-operation/geeorm/geeorm.go new file mode 100644 index 0000000..61ee9e0 --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/geeorm.go @@ -0,0 +1,51 @@ +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) +} diff --git a/gee-orm/day4-chain-operation/geeorm/geeorm_test.go b/gee-orm/day4-chain-operation/geeorm/geeorm_test.go new file mode 100644 index 0000000..35628be --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/geeorm_test.go @@ -0,0 +1,20 @@ +package geeorm + +import ( + _ "github.com/mattn/go-sqlite3" + "testing" +) + +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 TestNewEngine(t *testing.T) { + engine := OpenDB(t) + _ = engine.Close() +} diff --git a/gee-orm/day4-chain-operation/geeorm/go.mod b/gee-orm/day4-chain-operation/geeorm/go.mod new file mode 100644 index 0000000..043b1c6 --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/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/day4-chain-operation/geeorm/log/log.go b/gee-orm/day4-chain-operation/geeorm/log/log.go new file mode 100644 index 0000000..684a718 --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/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 = 0 + ErrorLevel = 1 + Disabled = 9999 +) + +// 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/day4-chain-operation/geeorm/log/log_test.go b/gee-orm/day4-chain-operation/geeorm/log/log_test.go new file mode 100644 index 0000000..8cd403c --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/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/day4-chain-operation/geeorm/schema/schema.go b/gee-orm/day4-chain-operation/geeorm/schema/schema.go new file mode 100644 index 0000000..8519fd4 --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/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/day4-chain-operation/geeorm/schema/schema_test.go b/gee-orm/day4-chain-operation/geeorm/schema/schema_test.go new file mode 100644 index 0000000..aba3e0b --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/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/day4-chain-operation/geeorm/session/raw.go b/gee-orm/day4-chain-operation/geeorm/session/raw.go new file mode 100644 index 0000000..6cb72c9 --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/session/raw.go @@ -0,0 +1,59 @@ +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 + 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, + } +} + +// Exec raw sql with sqlVars +func (s *Session) Exec() (result sql.Result, err error) { + 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 { + 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) { + 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/day4-chain-operation/geeorm/session/raw_test.go b/gee-orm/day4-chain-operation/geeorm/session/raw_test.go new file mode 100644 index 0000000..ce1e75b --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/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/day4-chain-operation/geeorm/session/record.go b/gee-orm/day4-chain-operation/geeorm/session/record.go new file mode 100644 index 0000000..c9902d0 --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/session/record.go @@ -0,0 +1,80 @@ +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 +} diff --git a/gee-orm/day4-chain-operation/geeorm/session/record_test.go b/gee-orm/day4-chain-operation/geeorm/session/record_test.go new file mode 100644 index 0000000..5879cfb --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/session/record_test.go @@ -0,0 +1,76 @@ +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") + } + +} diff --git a/gee-orm/day4-chain-operation/geeorm/session/schema.go b/gee-orm/day4-chain-operation/geeorm/session/schema.go new file mode 100644 index 0000000..11ff63b --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/session/schema.go @@ -0,0 +1,56 @@ +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 +} + +// HasTable returns true of the table exists +func (s *Session) HasTable(value interface{}) bool { + tableName, ok := value.(string) + if !ok { + tableName = s.RefTable(value).TableName + } + + 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/day4-chain-operation/geeorm/session/schema_test.go b/gee-orm/day4-chain-operation/geeorm/session/schema_test.go new file mode 100644 index 0000000..5c934fa --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/session/schema_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") + } +}