diff --git a/.gitignore b/.gitignore index ae93b1b..d9335e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ .DS_Store -tmp \ No newline at end of file +.idea +.vscode +tmp +*.db +*.sum \ No newline at end of file diff --git a/gee-orm/day1-database-sql/geeorm/dialect/dialect.go b/gee-orm/day1-database-sql/geeorm/dialect/dialect.go new file mode 100644 index 0000000..ee07db3 --- /dev/null +++ b/gee-orm/day1-database-sql/geeorm/dialect/dialect.go @@ -0,0 +1,19 @@ +package dialect + +import "reflect" + +var dialectsMap = map[string]Dialect{} + +type Dialect interface { + DataTypeOf(typ reflect.Value) string + PrimaryKeyTag(key string) string +} + +func RegisterDialect(name string, dialect Dialect) { + dialectsMap[name] = dialect +} + +func GetDialect(name string) (dialect Dialect, ok bool) { + dialect, ok = dialectsMap[name] + return +} diff --git a/gee-orm/day1-database-sql/geeorm/dialect/sqlite3.go b/gee-orm/day1-database-sql/geeorm/dialect/sqlite3.go new file mode 100644 index 0000000..8eca05a --- /dev/null +++ b/gee-orm/day1-database-sql/geeorm/dialect/sqlite3.go @@ -0,0 +1,43 @@ +package dialect + +import ( + "fmt" + "reflect" + "time" +) + +type sqlite3 struct{} + +var _ Dialect = (*sqlite3)(nil) + +func init() { + RegisterDialect("sqlite3", &sqlite3{}) +} + +// Get Data Type for Sqlite 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())) +} + +func (s *sqlite3) PrimaryKeyTag(key string) string { + return "INTEGER PRIMARY KEY AUTOINCREMENT" +} diff --git a/gee-orm/day1-database-sql/geeorm/dialect/sqlite3_test.go b/gee-orm/day1-database-sql/geeorm/dialect/sqlite3_test.go new file mode 100644 index 0000000..3df5f07 --- /dev/null +++ b/gee-orm/day1-database-sql/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/day1-database-sql/geeorm/geeorm.go b/gee-orm/day1-database-sql/geeorm/geeorm.go new file mode 100644 index 0000000..a1d1f7e --- /dev/null +++ b/gee-orm/day1-database-sql/geeorm/geeorm.go @@ -0,0 +1,67 @@ +package geeorm + +import ( + "database/sql" + "fmt" + "log" + "os" + + "geeorm/dialect" + "geeorm/schema" +) + +var ( + ErrorLog = log.New(os.Stdout, "[error] ", log.LstdFlags) + InfoLog = log.New(os.Stdout, "[info ] ", log.LstdFlags) +) + +type Engine struct { + db *sql.DB + dialect dialect.Dialect +} + +func NewEngine(driver, source string) (e *Engine, err error) { + db, err := sql.Open(driver, source) + if err != nil { + ErrorLog.Println(err) + return + } + // Send a ping to make sure the database connection is alive. + if err = db.Ping(); err != nil { + ErrorLog.Println(err) + return + } + // make sure the specific dialect exists + dial, ok := dialect.GetDialect(driver) + if !ok { + err = fmt.Errorf("dialect %s Not Found", driver) + ErrorLog.Println(err) + return + } + e = &Engine{db: db, dialect: dial} + InfoLog.Println("Connect database success") + return +} + +func (e *Engine) Close() (err error) { + if err = e.db.Close(); err == nil { + InfoLog.Println("Close database success") + } + return +} + +func (e *Engine) CreateTable(value interface{}) error { + _, err := e.NewSession(value).CreateTable().Exec() + return err +} + +func (e *Engine) NewSession(value interface{}) *Session { + var refTable *schema.Schema + if value != nil { + refTable = schema.Parse(value, e.dialect) + } + return &Session{ + refTable: refTable, + engine: e, + } +} diff --git a/gee-orm/day1-database-sql/geeorm/geeorm_test.go b/gee-orm/day1-database-sql/geeorm/geeorm_test.go new file mode 100644 index 0000000..c6da191 --- /dev/null +++ b/gee-orm/day1-database-sql/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) + defer engine.Close() +} diff --git a/gee-orm/day1-database-sql/geeorm/go.mod b/gee-orm/day1-database-sql/geeorm/go.mod new file mode 100644 index 0000000..043b1c6 --- /dev/null +++ b/gee-orm/day1-database-sql/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/day1-database-sql/geeorm/schema/field.go b/gee-orm/day1-database-sql/geeorm/schema/field.go new file mode 100644 index 0000000..46e82e4 --- /dev/null +++ b/gee-orm/day1-database-sql/geeorm/schema/field.go @@ -0,0 +1,13 @@ +package schema + +import "fmt" + +type Field struct { + Name string + Value interface{} + Tag string +} + +func (f *Field) String() string { + return fmt.Sprintf("%s %s", f.Name, f.Tag) +} diff --git a/gee-orm/day1-database-sql/geeorm/schema/schema.go b/gee-orm/day1-database-sql/geeorm/schema/schema.go new file mode 100644 index 0000000..ae8ea7b --- /dev/null +++ b/gee-orm/day1-database-sql/geeorm/schema/schema.go @@ -0,0 +1,45 @@ +package schema + +import ( + "fmt" + "go/ast" + "reflect" + "strings" + + "geeorm/dialect" +) + +type Schema struct { + Table string + PrimaryField *Field + Fields []*Field +} + +func Parse(dest interface{}, d dialect.Dialect) *Schema { + modelType := reflect.Indirect(reflect.ValueOf(dest)).Type() + + schema := &Schema{ + Table: modelType.Name(), + PrimaryField: &Field{Name: "ID", Value: 0}, + } + + for i := 0; i < modelType.NumField(); i++ { + p := modelType.Field(i) + if !p.Anonymous && ast.IsExported(p.Name) { + schema.Fields = append(schema.Fields, &Field{ + Name: p.Name, + Tag: d.DataTypeOf(reflect.Indirect(reflect.New(p.Type))), + }) + } + } + return schema +} + +func (s *Schema) String() string { + var fieldStr []string + for _, field := range s.Fields { + fieldStr = append(fieldStr, field.String()) + } + + return fmt.Sprintf("TABLE %s(%s)", s.Table, strings.Join(fieldStr, ", ")) +} diff --git a/gee-orm/day1-database-sql/geeorm/schema/schema_test.go b/gee-orm/day1-database-sql/geeorm/schema/schema_test.go new file mode 100644 index 0000000..5e1ad2d --- /dev/null +++ b/gee-orm/day1-database-sql/geeorm/schema/schema_test.go @@ -0,0 +1,20 @@ +package schema + +import ( + "geeorm/dialect" + "testing" +) + +type User struct { + Name string + Age int +} + +func TestParse(t *testing.T) { + dial, _ := dialect.GetDialect("sqlite3") + schema := Parse(&User{"Tom", 18}, dial) + + if schema.Table != "User" || len(schema.Fields) != 2 { + t.Fatal("failed to parse User struct") + } +} diff --git a/gee-orm/day1-database-sql/geeorm/session.go b/gee-orm/day1-database-sql/geeorm/session.go new file mode 100644 index 0000000..f1cd36a --- /dev/null +++ b/gee-orm/day1-database-sql/geeorm/session.go @@ -0,0 +1,48 @@ +package geeorm + +import ( + "database/sql" + "fmt" + "strings" + + "geeorm/schema" +) + +type Session struct { + engine *Engine + refTable *schema.Schema + + Value interface{} + SQL strings.Builder + SQLVars []interface{} +} + +func (s *Session) Exec() (result sql.Result, err error) { + if result, err = s.engine.db.Exec(s.SQL.String(), s.SQLVars...); err != nil { + ErrorLog.Println(err) + } + return +} + +func (s *Session) QueryRows() (rows *sql.Rows, err error) { + if rows, err = s.engine.db.Query(s.SQL.String(), s.SQLVars...); err != nil { + ErrorLog.Println(err) + } + return +} + +func (s *Session) Raw(sql string, values ...interface{}) *Session { + s.SQL.WriteString(sql) + s.SQLVars = values + return s +} + +func (s *Session) CreateTable() *Session { + var columns []string + for _, field := range s.refTable.Fields { + columns = append(columns, fmt.Sprintf("%s %s", field.Name, field.Tag)) + } + desc := strings.Join(columns, ",") + s.SQL.WriteString(fmt.Sprintf("CREATE TABLE %s (%s);", s.refTable.Table, desc)) + return s +} diff --git a/gee-orm/day1-database-sql/geeorm/session_test.go b/gee-orm/day1-database-sql/geeorm/session_test.go new file mode 100644 index 0000000..ea30d7c --- /dev/null +++ b/gee-orm/day1-database-sql/geeorm/session_test.go @@ -0,0 +1,29 @@ +package geeorm + +import "testing" + +func TestExec(t *testing.T) { + engine := OpenDB(t) + defer engine.Close() + engine.NewSession(nil).Raw("DROP TABLE USER;").Exec() + engine.NewSession(nil).Raw("CREATE TABLE USER(name text);").Exec() + result, _ := engine.NewSession(nil).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 TestQuery(t *testing.T) { + engine := OpenDB(t) + defer engine.Close() + engine.NewSession(nil).Raw("DROP TABLE USER;").Exec() + engine.NewSession(nil).Raw("CREATE TABLE USER(name text);").Exec() + rows, _ := engine.NewSession(nil).Raw("SELECT count(*) FROM USER").QueryRows() + defer rows.Close() + var count int + for rows.Next() { + if err := rows.Scan(&count); err != nil || count != 0 { + t.Fatal("failed to query db", err) + } + } +}