├── oracle_test.go ├── LICENSE ├── mysql.go ├── example └── example.go ├── criteria.go ├── assert.go ├── migration.go ├── benchmark_test_util.go ├── dialect.go ├── model_test.go ├── syntax_test_util.go ├── postgres.go ├── oracle.go ├── sqlite3.go ├── postgres_test.go ├── mysql_test.go ├── sqlite3_test.go ├── README_ZH.md ├── base.go ├── model.go ├── README.md ├── db_test_util.go └── qbs.go /oracle_test.go: -------------------------------------------------------------------------------- 1 | package qbs 2 | 3 | import ( 4 | "testing" 5 | 6 | // "time" 7 | ) 8 | 9 | var oracleSqlTypeResults []string = []string{ 10 | "-", 11 | "NUMBER", 12 | "NUMBER", 13 | "NUMBER", 14 | "NUMBER", 15 | "NUMBER", 16 | "NUMBER", 17 | "NUMBER", 18 | "NUMBER", 19 | "NUMBER", 20 | "NUMBER", 21 | "NUMBER(16,2)", 22 | "NUMBER(16,2)", 23 | "VARCHAR2(128)", 24 | "CLOB", 25 | "DATE", 26 | "CLOB", 27 | "NUMBER", 28 | "NUMBER", 29 | "-", 30 | "NUMBER(16,2)", 31 | "DATE", 32 | "VARCHAR2(128)", 33 | "CLOB", 34 | } 35 | 36 | func TestSqlTypeForOrDialect(t *testing.T) { 37 | assert := NewAssert(t) 38 | d := NewOracle() 39 | //omitFields := []string{"Bool", "DerivedBool"} 40 | testModel := structPtrToModel(new(typeTestTable), false, nil) 41 | for index, column := range testModel.fields { 42 | if storedResult := oracleSqlTypeResults[index]; storedResult != "-" { 43 | result := d.sqlType(*column) 44 | assert.Equal(storedResult, result) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2013 Ewan Chou. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /mysql.go: -------------------------------------------------------------------------------- 1 | package qbs 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "reflect" 7 | "time" 8 | ) 9 | 10 | type mysql struct { 11 | base 12 | } 13 | 14 | func NewMysql() Dialect { 15 | d := new(mysql) 16 | d.base.dialect = d 17 | return d 18 | } 19 | 20 | func DefaultMysqlDataSourceName(dbName string) *DataSourceName { 21 | dsn := new(DataSourceName) 22 | dsn.Dialect = new(mysql) 23 | dsn.Username = "root" 24 | dsn.DbName = dbName 25 | dsn.Append("loc", "Local") 26 | dsn.Append("charset", "utf8") 27 | dsn.Append("parseTime", "true") 28 | return dsn 29 | } 30 | 31 | func (d mysql) parseBool(value reflect.Value) bool { 32 | return value.Int() != 0 33 | } 34 | 35 | func (d mysql) sqlType(field modelField) string { 36 | f := field.value 37 | fieldValue := reflect.ValueOf(f) 38 | kind := fieldValue.Kind() 39 | if field.nullable != reflect.Invalid { 40 | kind = field.nullable 41 | } 42 | switch kind { 43 | case reflect.Bool: 44 | return "boolean" 45 | case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint8, reflect.Uint16, reflect.Uint32: 46 | return "int" 47 | case reflect.Uint, reflect.Uint64, reflect.Int, reflect.Int64: 48 | return "bigint" 49 | case reflect.Float32, reflect.Float64: 50 | return "double" 51 | case reflect.String: 52 | if field.size > 0 && field.size < 65532 { 53 | return fmt.Sprintf("varchar(%d)", field.size) 54 | } 55 | return "longtext" 56 | case reflect.Slice: 57 | if reflect.TypeOf(f).Elem().Kind() == reflect.Uint8 { 58 | if field.size > 0 && field.size < 65532 { 59 | return fmt.Sprintf("varbinary(%d)", field.size) 60 | } 61 | return "longblob" 62 | } 63 | case reflect.Struct: 64 | switch fieldValue.Interface().(type) { 65 | case time.Time: 66 | return "timestamp" 67 | case sql.NullBool: 68 | return "boolean" 69 | case sql.NullInt64: 70 | return "bigint" 71 | case sql.NullFloat64: 72 | return "double" 73 | case sql.NullString: 74 | if field.size > 0 && field.size < 65532 { 75 | return fmt.Sprintf("varchar(%d)", field.size) 76 | } 77 | return "longtext" 78 | default: 79 | if len(field.colType) != 0 { 80 | switch field.colType { 81 | case QBS_COLTYPE_BOOL, QBS_COLTYPE_INT, QBS_COLTYPE_BIGINT, QBS_COLTYPE_DOUBLE, QBS_COLTYPE_TIME: 82 | return field.colType 83 | case QBS_COLTYPE_TEXT: 84 | if field.size > 0 && field.size < 65532 { 85 | return fmt.Sprintf("varchar(%d)", field.size) 86 | } 87 | return "longtext" 88 | default: 89 | panic("Qbs doesn't support column type " + field.colType + " for MySQL") 90 | } 91 | } 92 | } 93 | } 94 | panic("invalid sql type for field:" + field.name) 95 | } 96 | 97 | func (d mysql) indexExists(mg *Migration, tableName, indexName string) bool { 98 | var row *sql.Row 99 | var name string 100 | row = mg.db.QueryRow("SELECT INDEX_NAME FROM INFORMATION_SCHEMA.STATISTICS "+ 101 | "WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND INDEX_NAME = ?", mg.dbName, tableName, indexName) 102 | row.Scan(&name) 103 | return name != "" 104 | } 105 | 106 | func (d mysql) primaryKeySql(isString bool, size int) string { 107 | if isString { 108 | return fmt.Sprintf("varchar(%d) PRIMARY KEY", size) 109 | } 110 | return "bigint PRIMARY KEY AUTO_INCREMENT" 111 | } 112 | -------------------------------------------------------------------------------- /example/example.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | _ "github.com/coocood/mysql" 7 | "github.com/coocood/qbs" 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | type User struct { 13 | Id int64 14 | Name string `qbs:"size:50,index"` 15 | } 16 | 17 | func (*User) Indexes(indexes *qbs.Indexes) { 18 | //indexes.Add("column_a", "column_b") or indexes.AddUnique("column_a", "column_b") 19 | } 20 | 21 | type Post struct { 22 | Id int64 23 | AuthorId int64 24 | Author *User 25 | Content string 26 | Created time.Time 27 | Updated time.Time 28 | } 29 | 30 | func RegisterDb() { 31 | qbs.Register("mysql", "qbs_test@/qbs_test?charset=utf8&parseTime=true&loc=Local", "qbs_test", qbs.NewMysql()) 32 | } 33 | 34 | func GetUser(w http.ResponseWriter, r *http.Request) { 35 | q, err := qbs.GetQbs() 36 | if err != nil { 37 | fmt.Println(err) 38 | w.WriteHeader(500) 39 | return 40 | } 41 | defer q.Close() 42 | u, err := FindUserById(q, 6) 43 | data, _ := json.Marshal(u) 44 | w.Write(data) 45 | } 46 | 47 | func CreateUserTable() error { 48 | migration, err := qbs.GetMigration() 49 | if err != nil { 50 | return err 51 | } 52 | defer migration.Close() 53 | return migration.CreateTableIfNotExists(new(User)) 54 | } 55 | 56 | func CreateUser(q *qbs.Qbs) (*User, error) { 57 | user := new(User) 58 | user.Name = "Green" 59 | _, err := q.Save(user) 60 | return user, err 61 | } 62 | 63 | func FindUserById(q *qbs.Qbs, id int64) (*User, error) { 64 | user := new(User) 65 | user.Id = id 66 | err := q.Find(user) 67 | return user, err 68 | } 69 | 70 | func FindUserByName(q *qbs.Qbs, n string) (*User, error) { 71 | user := new(User) 72 | err := q.WhereEqual("name", n).Find(user) 73 | return user, err 74 | } 75 | 76 | func FindUserByCondition(q *qbs.Qbs) (*User, error) { 77 | user := new(User) 78 | condition1 := qbs.NewCondition("id > ?", 100).Or("id < ?", 50).OrEqual("id", 75) 79 | condition2 := qbs.NewCondition("name != ?", "Red").And("name != ?", "Black") 80 | condition1.AndCondition(condition2) 81 | err := q.Condition(condition1).Find(user) 82 | return user, err 83 | } 84 | 85 | func FindUsers(q *qbs.Qbs) ([]*User, error) { 86 | var users []*User 87 | err := q.Limit(10).Offset(10).FindAll(&users) 88 | return users, err 89 | } 90 | 91 | func UpdateOneUser(q *qbs.Qbs, id int64, name string) (affected int64, err error) { 92 | user, err := FindUserById(q, id) 93 | if err != nil { 94 | return 0, err 95 | } 96 | user.Name = name 97 | return q.Save(user) 98 | } 99 | 100 | func UpdateMultipleUsers(q *qbs.Qbs) (affected int64, err error) { 101 | type User struct { 102 | Name string 103 | } 104 | user := new(User) 105 | user.Name = "Blue" 106 | return q.WhereEqual("name", "Green").Update(user) 107 | } 108 | 109 | func DeleteUser(q *qbs.Qbs, id int64) (affected int64, err error) { 110 | user := new(User) 111 | user.Id = id 112 | return q.Delete(user) 113 | } 114 | 115 | func FindPostsOmitContentAndCreated(q *qbs.Qbs) ([]*Post, error) { 116 | var posts []*Post 117 | err := q.OmitFields("Content", "Created").Find(&posts) 118 | return posts, err 119 | } 120 | 121 | func FindPostsOmitJoin(q *qbs.Qbs) ([]*Post, error) { 122 | var posts []*Post 123 | err := q.OmitJoin().OmitFields("Content").Find(&posts) 124 | return posts, err 125 | } 126 | -------------------------------------------------------------------------------- /criteria.go: -------------------------------------------------------------------------------- 1 | package qbs 2 | 3 | type criteria struct { 4 | model *model 5 | condition *Condition 6 | orderBys []order 7 | limit int 8 | offset int 9 | omitFields []string 10 | omitJoin bool 11 | } 12 | 13 | func (c *criteria) mergePkCondition(d Dialect) { 14 | var con *Condition 15 | if !c.model.pkZero() { 16 | expr := d.quote(c.model.pk.name) + " = ?" 17 | con = NewCondition(expr, c.model.pk.value) 18 | con.AndCondition(c.condition) 19 | } else { 20 | con = c.condition 21 | } 22 | c.condition = con 23 | } 24 | 25 | type order struct { 26 | path string 27 | desc bool 28 | } 29 | 30 | // Conditions are structured in a way to define 31 | // complex where clause easily. 32 | type Condition struct { 33 | expr string 34 | args []interface{} 35 | sub *Condition 36 | isOr bool 37 | } 38 | 39 | func NewCondition(expr string, args ...interface{}) *Condition { 40 | return &Condition{ 41 | expr: expr, 42 | args: args, 43 | } 44 | } 45 | 46 | //Snakecase column name 47 | func NewEqualCondition(column string, value interface{}) *Condition { 48 | expr := column + " = ?" 49 | return NewCondition(expr, value) 50 | } 51 | 52 | func NewInCondition(column string, values []interface{}) *Condition { 53 | expr := column + " IN (" 54 | for _ = range values { 55 | expr += "?, " 56 | } 57 | expr = expr[:len(expr)-2] 58 | expr += ")" 59 | return &Condition{ 60 | expr: expr, 61 | args: values, 62 | } 63 | } 64 | 65 | func (c *Condition) And(expr string, args ...interface{}) *Condition { 66 | if c.sub != nil { 67 | c.expr, c.args = c.Merge() 68 | } 69 | c.sub = NewCondition(expr, args...) 70 | c.isOr = false 71 | return c 72 | } 73 | 74 | //Snakecase column name 75 | func (c *Condition) AndEqual(column string, value interface{}) *Condition { 76 | expr := column + " = ?" 77 | c.And(expr, value) 78 | return c 79 | } 80 | 81 | func (c *Condition) AndCondition(subCondition *Condition) *Condition { 82 | if c.sub != nil { 83 | c.expr, c.args = c.Merge() 84 | } 85 | c.sub = subCondition 86 | c.isOr = false 87 | return c 88 | } 89 | 90 | func (c *Condition) Or(expr string, args ...interface{}) *Condition { 91 | if c.sub != nil { 92 | c.expr, c.args = c.Merge() 93 | } 94 | c.sub = NewCondition(expr, args...) 95 | c.isOr = true 96 | return c 97 | } 98 | 99 | //Snakecase column name 100 | func (c *Condition) OrEqual(column string, value interface{}) *Condition { 101 | expr := column + " = ?" 102 | c.Or(expr, value) 103 | return c 104 | } 105 | 106 | func (c *Condition) OrCondition(subCondition *Condition) *Condition { 107 | if c.sub != nil { 108 | c.expr, c.args = c.Merge() 109 | } 110 | c.sub = subCondition 111 | c.isOr = true 112 | return c 113 | } 114 | 115 | func (c *Condition) Merge() (expr string, args []interface{}) { 116 | expr = c.expr 117 | args = c.args 118 | if c.sub == nil { 119 | return 120 | } 121 | expr = "(" + expr + ")" 122 | if c.isOr { 123 | expr += " OR " 124 | } else { 125 | expr += " AND " 126 | } 127 | subExpr, subArgs := c.sub.Merge() 128 | expr += "(" + subExpr + ")" 129 | args = append(args, subArgs...) 130 | return expr, args 131 | } 132 | 133 | //Used for in condition. 134 | func StringsToInterfaces(strs ...string) []interface{} { 135 | ret := make([]interface{}, len(strs)) 136 | for i := 0; i < len(strs); i++ { 137 | ret[i] = strs[i] 138 | } 139 | return ret 140 | } 141 | 142 | func IntsToInterfaces(ints ...int64) []interface{} { 143 | ret := make([]interface{}, len(ints)) 144 | for i := 0; i < len(ints); i++ { 145 | ret[i] = ints[i] 146 | } 147 | return ret 148 | } 149 | -------------------------------------------------------------------------------- /assert.go: -------------------------------------------------------------------------------- 1 | package qbs 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "runtime" 7 | ) 8 | 9 | type Assert struct { 10 | tester 11 | } 12 | 13 | type tester interface { 14 | Fail() 15 | Failed() bool 16 | FailNow() 17 | Log(args ...interface{}) 18 | Logf(format string, args ...interface{}) 19 | Error(args ...interface{}) 20 | Errorf(format string, args ...interface{}) 21 | Fatal(args ...interface{}) 22 | Fatalf(format string, args ...interface{}) 23 | Skip(args ...interface{}) 24 | Skipf(format string, args ...interface{}) 25 | SkipNow() 26 | Skipped() bool 27 | } 28 | 29 | type benchmarker interface { 30 | StartTimer() 31 | StopTimer() 32 | } 33 | 34 | func NewAssert(t tester) *Assert { 35 | return &Assert{t} 36 | } 37 | 38 | func (ast *Assert) Nil(value interface{}, logs ...interface{}) { 39 | ast.nilAssert(false, true, value, logs...) 40 | } 41 | 42 | func (ast *Assert) MustNil(value interface{}, logs ...interface{}) { 43 | ast.nilAssert(true, true, value, logs...) 44 | } 45 | 46 | func (ast *Assert) NotNil(value interface{}, logs ...interface{}) { 47 | ast.nilAssert(false, false, value, logs...) 48 | } 49 | 50 | func (ast *Assert) MustNotNil(value interface{}, logs ...interface{}) { 51 | ast.nilAssert(true, false, value, logs...) 52 | } 53 | 54 | func (ast *Assert) nilAssert(fatal bool, isNil bool, value interface{}, logs ...interface{}) { 55 | if isNil != (value == nil || reflect.ValueOf(value).IsNil()) { 56 | ast.logCaller() 57 | if len(logs) > 0 { 58 | ast.Log(logs...) 59 | } else { 60 | if isNil { 61 | ast.Log("value is not nil:", value) 62 | } else { 63 | ast.Log("value is nil") 64 | } 65 | } 66 | ast.failIt(fatal) 67 | } 68 | } 69 | 70 | func (ast *Assert) True(boolValue bool, logs ...interface{}) { 71 | ast.trueAssert(false, boolValue, logs...) 72 | } 73 | 74 | func (ast *Assert) MustTrue(boolValue bool, logs ...interface{}) { 75 | ast.trueAssert(true, boolValue, logs...) 76 | } 77 | 78 | func (ast *Assert) trueAssert(fatal bool, value bool, logs ...interface{}) { 79 | if !value { 80 | ast.logCaller() 81 | if len(logs) > 0 { 82 | ast.Log(logs...) 83 | } else { 84 | ast.Logf("value is not true") 85 | } 86 | ast.failIt(fatal) 87 | } 88 | } 89 | 90 | func (ast *Assert) Equal(expected, actual interface{}, logs ...interface{}) { 91 | ast.equalSprintAssert(false, true, expected, actual, logs...) 92 | } 93 | 94 | func (ast *Assert) MustEqual(expected, actual interface{}, logs ...interface{}) { 95 | ast.equalSprintAssert(true, true, expected, actual, logs...) 96 | } 97 | 98 | func (ast *Assert) NotEqual(expected, actual interface{}, logs ...interface{}) { 99 | ast.equalSprintAssert(false, false, expected, actual, logs...) 100 | } 101 | 102 | func (ast *Assert) MustNotEqual(expected, actual interface{}, logs ...interface{}) { 103 | ast.equalSprintAssert(true, false, expected, actual, logs...) 104 | } 105 | 106 | func (ast *Assert) equalSprintAssert(fatal bool, isEqual bool, expected, actual interface{}, logs ...interface{}) { 107 | expectedStr := fmt.Sprint(expected) 108 | actualStr := fmt.Sprint(actual) 109 | if isEqual != (expectedStr == actualStr) { 110 | ast.logCaller() 111 | if len(logs) > 0 { 112 | ast.Log(logs...) 113 | } else { 114 | if isEqual { 115 | ast.Log("Values not equal") 116 | } else { 117 | ast.Log("Values equal") 118 | } 119 | } 120 | ast.Log("Expected: ", expected) 121 | ast.Log("Actual: ", actual) 122 | ast.failIt(fatal) 123 | } 124 | } 125 | 126 | func (ast *Assert) logCaller() { 127 | _, file, line, _ := runtime.Caller(3) 128 | ast.Logf("Caller: %v:%d", file, line) 129 | } 130 | 131 | func (ast *Assert) failIt(fatal bool) { 132 | if fatal { 133 | ast.FailNow() 134 | } else { 135 | ast.Fail() 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /migration.go: -------------------------------------------------------------------------------- 1 | package qbs 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | type Migration struct { 10 | db *sql.DB 11 | dbName string 12 | dialect Dialect 13 | Log bool 14 | } 15 | 16 | // CreateTableIfNotExists creates a new table and its indexes based on the table struct type 17 | // It will panic if table creation failed, and it will return error if the index creation failed. 18 | func (mg *Migration) CreateTableIfNotExists(structPtr interface{}) error { 19 | model := structPtrToModel(structPtr, true, nil) 20 | sql := mg.dialect.createTableSql(model, true) 21 | if mg.Log { 22 | fmt.Println(sql) 23 | } 24 | sqls := strings.Split(sql, ";") 25 | for _, v := range sqls { 26 | _, err := mg.db.Exec(v) 27 | if err != nil && !mg.dialect.catchMigrationError(err) { 28 | panic(err) 29 | } 30 | } 31 | columns := mg.dialect.columnsInTable(mg, model.table) 32 | if len(model.fields) > len(columns) { 33 | oldFields := []*modelField{} 34 | newFields := []*modelField{} 35 | for _, v := range model.fields { 36 | if _, ok := columns[v.name]; ok { 37 | oldFields = append(oldFields, v) 38 | } else { 39 | newFields = append(newFields, v) 40 | } 41 | } 42 | if len(oldFields) != len(columns) { 43 | panic("Column name has changed, rename column migration is not supported.") 44 | } 45 | for _, v := range newFields { 46 | mg.addColumn(model.table, v) 47 | } 48 | } 49 | var indexErr error 50 | for _, i := range model.indexes { 51 | indexErr = mg.CreateIndexIfNotExists(model.table, i.name, i.unique, i.columns...) 52 | } 53 | return indexErr 54 | } 55 | 56 | // this is only used for testing. 57 | func (mg *Migration) dropTableIfExists(structPtr interface{}) { 58 | tn := tableName(structPtr) 59 | _, err := mg.db.Exec(mg.dialect.dropTableSql(tn)) 60 | if err != nil && !mg.dialect.catchMigrationError(err) { 61 | panic(err) 62 | } 63 | } 64 | 65 | //Can only drop table on database which name has "test" suffix. 66 | //Used for testing 67 | func (mg *Migration) DropTable(strutPtr interface{}) { 68 | if !strings.HasSuffix(mg.dbName, "test") { 69 | panic("Drop table can only be executed on database which name has 'test' suffix") 70 | } 71 | mg.dropTableIfExists(strutPtr) 72 | } 73 | 74 | func (mg *Migration) addColumn(table string, column *modelField) { 75 | sql := mg.dialect.addColumnSql(table, *column) 76 | if mg.Log { 77 | fmt.Println(sql) 78 | } 79 | _, err := mg.db.Exec(sql) 80 | if err != nil { 81 | panic(err) 82 | } 83 | } 84 | 85 | // CreateIndex creates the specified index on table. 86 | // Some databases like mysql do not support this feature directly, 87 | // So dialect may need to query the database schema table to find out if an index exists. 88 | // Normally you don't need to do it explicitly, it will be created automatically in CreateTableIfNotExists method. 89 | func (mg *Migration) CreateIndexIfNotExists(table interface{}, name string, unique bool, columns ...string) error { 90 | tn := tableName(table) 91 | name = tn + "_" + name 92 | if !mg.dialect.indexExists(mg, tn, name) { 93 | sql := mg.dialect.createIndexSql(name, tn, unique, columns...) 94 | if mg.Log { 95 | fmt.Println(sql) 96 | } 97 | _, err := mg.db.Exec(sql) 98 | return err 99 | } 100 | return nil 101 | } 102 | 103 | func (mg *Migration) Close() { 104 | if mg.db != nil { 105 | err := mg.db.Close() 106 | if err != nil { 107 | panic(err) 108 | } 109 | } 110 | } 111 | 112 | // Get a Migration instance should get closed like Qbs instance. 113 | func GetMigration() (mg *Migration, err error) { 114 | if driver == "" || dial == nil { 115 | panic("database driver has not been registered, should call Register first.") 116 | } 117 | db, err := sql.Open(driver, driverSource) 118 | if err != nil { 119 | return nil, err 120 | } 121 | return &Migration{db, dbName, dial, false}, nil 122 | } 123 | 124 | // A safe and easy way to work with Migration instance without the need to open and close it. 125 | func WithMigration(task func(mg *Migration) error) error { 126 | mg, err := GetMigration() 127 | if err != nil { 128 | return err 129 | } 130 | defer mg.Close() 131 | return task(mg) 132 | } 133 | -------------------------------------------------------------------------------- /benchmark_test_util.go: -------------------------------------------------------------------------------- 1 | package qbs 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "runtime" 7 | ) 8 | 9 | func doBenchmarkFind(b benchmarker, n int) { 10 | b.StopTimer() 11 | bas := new(basic) 12 | bas.Name = "Basic" 13 | bas.State = 3 14 | mg, _ := GetMigration() 15 | mg.DropTable(bas) 16 | mg.CreateTableIfNotExists(bas) 17 | q, _ := GetQbs() 18 | q.Save(bas) 19 | closeMigrationAndQbs(mg, q) 20 | b.StartTimer() 21 | for i := 0; i < n; i++ { 22 | ba := new(basic) 23 | ba.Id = 1 24 | q, _ = GetQbs() 25 | err := q.Find(ba) 26 | if err != nil { 27 | panic(err) 28 | } 29 | q.Close() 30 | } 31 | b.StopTimer() 32 | runtime.GC() 33 | stats := new(runtime.MemStats) 34 | runtime.ReadMemStats(stats) 35 | fmt.Printf("alloc:%d, total:%d\n", stats.Alloc, stats.TotalAlloc) 36 | } 37 | 38 | func doBenchmarkQueryStruct(b benchmarker, n int) { 39 | b.StopTimer() 40 | bas := new(basic) 41 | bas.Name = "Basic" 42 | bas.State = 3 43 | mg, _ := GetMigration() 44 | mg.DropTable(bas) 45 | mg.CreateTableIfNotExists(bas) 46 | q, _ := GetQbs() 47 | q.Save(bas) 48 | closeMigrationAndQbs(mg, q) 49 | b.StartTimer() 50 | for i := 0; i < n; i++ { 51 | ba := new(basic) 52 | q, _ = GetQbs() 53 | err := q.QueryStruct(ba, "SELECT * FROM basic WHERE id = ?", 1) 54 | if err != nil { 55 | panic(err) 56 | } 57 | q.Close() 58 | } 59 | b.StopTimer() 60 | runtime.GC() 61 | stats := new(runtime.MemStats) 62 | runtime.ReadMemStats(stats) 63 | fmt.Printf("alloc:%d, total:%d\n", stats.Alloc, stats.TotalAlloc) 64 | } 65 | 66 | func doBenchmarkStmtQuery(b benchmarker, n int) { 67 | b.StopTimer() 68 | bas := new(basic) 69 | bas.Name = "Basic" 70 | bas.State = 3 71 | mg, _ := GetMigration() 72 | mg.DropTable(bas) 73 | mg.CreateTableIfNotExists(bas) 74 | q, _ := GetQbs() 75 | q.Save(bas) 76 | closeMigrationAndQbs(mg, q) 77 | b.StartTimer() 78 | db, _ := sql.Open(driver, driverSource) 79 | query := q.Dialect.substituteMarkers("SELECT * FROM basic WHERE id = ?") 80 | stmt, _ := db.Prepare(query) 81 | for i := 0; i < n; i++ { 82 | ba := new(basic) 83 | rows, err := stmt.Query(1) 84 | if err != nil { 85 | panic(err) 86 | } 87 | rows.Next() 88 | err = rows.Scan(&ba.Id, &ba.Name, &ba.State) 89 | if err != nil { 90 | panic(err) 91 | } 92 | rows.Close() 93 | } 94 | stmt.Close() 95 | db.Close() 96 | b.StopTimer() 97 | runtime.GC() 98 | stats := new(runtime.MemStats) 99 | runtime.ReadMemStats(stats) 100 | fmt.Printf("alloc:%d, total:%d\n", stats.Alloc, stats.TotalAlloc) 101 | } 102 | 103 | func doBenchmarkDbQuery(b benchmarker, n int) { 104 | b.StopTimer() 105 | bas := new(basic) 106 | bas.Name = "Basic" 107 | bas.State = 3 108 | mg, _ := GetMigration() 109 | mg.DropTable(bas) 110 | mg.CreateTableIfNotExists(bas) 111 | q, _ := GetQbs() 112 | q.Save(bas) 113 | closeMigrationAndQbs(mg, q) 114 | b.StartTimer() 115 | db, _ := sql.Open(driver, driverSource) 116 | query := q.Dialect.substituteMarkers("SELECT * FROM basic WHERE id = ?") 117 | for i := 0; i < n; i++ { 118 | ba := new(basic) 119 | rows, err := db.Query(query, 1) 120 | if err != nil { 121 | panic(err) 122 | } 123 | rows.Next() 124 | err = rows.Scan(&ba.Id, &ba.Name, &ba.State) 125 | if err != nil { 126 | panic(err) 127 | } 128 | rows.Close() 129 | } 130 | db.Close() 131 | b.StopTimer() 132 | runtime.GC() 133 | stats := new(runtime.MemStats) 134 | runtime.ReadMemStats(stats) 135 | fmt.Printf("alloc:%d, total:%d\n", stats.Alloc, stats.TotalAlloc) 136 | } 137 | 138 | func doBenchmarkTransaction(b benchmarker, n int) { 139 | b.StopTimer() 140 | bas := new(basic) 141 | bas.Name = "Basic" 142 | bas.State = 3 143 | mg, _ := GetMigration() 144 | mg.DropTable(bas) 145 | mg.CreateTableIfNotExists(bas) 146 | q, _ := GetQbs() 147 | q.Save(bas) 148 | closeMigrationAndQbs(mg, q) 149 | b.StartTimer() 150 | for i := 0; i < n; i++ { 151 | ba := new(basic) 152 | ba.Id = 1 153 | q, _ = GetQbs() 154 | q.Begin() 155 | q.Find(ba) 156 | q.Commit() 157 | q.Close() 158 | } 159 | b.StopTimer() 160 | runtime.GC() 161 | stats := new(runtime.MemStats) 162 | runtime.ReadMemStats(stats) 163 | fmt.Printf("alloc:%d, total:%d\n", stats.Alloc, stats.TotalAlloc) 164 | } 165 | -------------------------------------------------------------------------------- /dialect.go: -------------------------------------------------------------------------------- 1 | package qbs 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | ) 8 | 9 | type Dialect interface { 10 | 11 | //Substitute "?" marker if database use other symbol as marker 12 | substituteMarkers(query string) string 13 | 14 | // Quote will quote identifiers in a SQL statement. 15 | quote(s string) string 16 | 17 | sqlType(field modelField) string 18 | 19 | parseBool(value reflect.Value) bool 20 | 21 | setModelValue(value reflect.Value, field reflect.Value) error 22 | 23 | querySql(criteria *criteria) (sql string, args []interface{}) 24 | 25 | insert(q *Qbs) (int64, error) 26 | 27 | insertSql(criteria *criteria) (sql string, args []interface{}) 28 | 29 | update(q *Qbs) (int64, error) 30 | 31 | updateSql(criteria *criteria) (string, []interface{}) 32 | 33 | delete(q *Qbs) (int64, error) 34 | 35 | deleteSql(criteria *criteria) (string, []interface{}) 36 | 37 | createTableSql(model *model, ifNotExists bool) string 38 | 39 | dropTableSql(table string) string 40 | 41 | addColumnSql(table string, column modelField) string 42 | 43 | createIndexSql(name, table string, unique bool, columns ...string) string 44 | 45 | indexExists(mg *Migration, tableName string, indexName string) bool 46 | 47 | columnsInTable(mg *Migration, tableName interface{}) map[string]bool 48 | 49 | primaryKeySql(isString bool, size int) string 50 | 51 | catchMigrationError(err error) bool 52 | } 53 | 54 | type DataSourceName struct { 55 | DbName string 56 | Username string 57 | Password string 58 | UnixSocket bool 59 | Host string 60 | Port string 61 | Variables []string 62 | Dialect Dialect 63 | } 64 | 65 | func (dsn *DataSourceName) String() string { 66 | if dsn.Dialect == nil { 67 | panic("DbDialect is not set") 68 | } 69 | switch dsn.Dialect.(type) { 70 | case *mysql: 71 | dsnformat := "%v@%v/%v%v" 72 | login := dsn.Username 73 | if dsn.Password != "" { 74 | login += ":" + dsn.Password 75 | } 76 | var address string 77 | if dsn.Host != "" { 78 | address = dsn.Host 79 | if dsn.Port != "" { 80 | address += ":" + dsn.Port 81 | } 82 | protocol := "tcp" 83 | if dsn.UnixSocket { 84 | protocol = "unix" 85 | } 86 | address = protocol + "(" + address + ")" 87 | } 88 | var variables string 89 | if dsn.Variables != nil { 90 | variables = "?" + strings.Join(dsn.Variables, "&") 91 | } 92 | return fmt.Sprintf(dsnformat, login, address, dsn.DbName, variables) 93 | case *sqlite3: 94 | return dsn.DbName 95 | case *postgres: 96 | pairs := []string{"user=" + dsn.Username} 97 | if dsn.Password != "" { 98 | pairs = append(pairs, "password="+dsn.Password) 99 | } 100 | if dsn.DbName != "" { 101 | pairs = append(pairs, "dbname="+dsn.DbName) 102 | } 103 | pairs = append(pairs, dsn.Variables...) 104 | if dsn.Host != "" { 105 | host := dsn.Host 106 | if dsn.UnixSocket { 107 | host = "/" + host 108 | } 109 | pairs = append(pairs, "host="+host) 110 | } 111 | if dsn.Port != "" { 112 | pairs = append(pairs, "port="+dsn.Port) 113 | } 114 | return strings.Join(pairs, " ") 115 | } 116 | panic("Unknown DbDialect.") 117 | } 118 | 119 | func (dsn *DataSourceName) Append(key, value string) *DataSourceName { 120 | dsn.Variables = append(dsn.Variables, key+"="+value) 121 | return dsn 122 | } 123 | 124 | func RegisterWithDataSourceName(dsn *DataSourceName) { 125 | var driverName string 126 | mustCloseDBForNewDatasource := false 127 | switch dsn.Dialect.(type) { 128 | case *mysql: 129 | driverName = "mysql" 130 | case *sqlite3: 131 | driverName = "sqlite3" 132 | mustCloseDBForNewDatasource = true 133 | case *postgres: 134 | driverName = "postgres" 135 | mustCloseDBForNewDatasource = true 136 | } 137 | dbName := dsn.DbName 138 | if driverName == "sqlite3" { 139 | dbName = "" 140 | } 141 | 142 | //XXX This appears to something related to the specific way the tests 143 | //XXX run and the db variable. If the tests are run independently (with -test.run) 144 | //XXX then the tests pass. However, they fail if the database has already 145 | //XXX been Registered and the db variable is not nil. 146 | //XXX This is only needed for postgres and sqlite3. 147 | if mustCloseDBForNewDatasource && db != nil { 148 | if err := db.Close(); err != nil { 149 | panic(err) 150 | } 151 | db = nil 152 | } 153 | Register(driverName, dsn.String(), dbName, dsn.Dialect) 154 | } 155 | -------------------------------------------------------------------------------- /model_test.go: -------------------------------------------------------------------------------- 1 | package qbs 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestParseTags(t *testing.T) { 10 | assert := NewAssert(t) 11 | fd := new(modelField) 12 | parseTags(fd, `fk:User`) 13 | assert.Equal("User", fd.fk) 14 | fd = new(modelField) 15 | parseTags(fd, `notnull,default:'banana'`) 16 | assert.True(fd.notnull) 17 | assert.Equal("'banana'", fd.dfault) 18 | } 19 | 20 | func TestFieldOmit(t *testing.T) { 21 | assert := NewAssert(t) 22 | type Schema struct { 23 | A string `qbs:"-"` 24 | B string 25 | C string 26 | } 27 | m := structPtrToModel(&Schema{}, true, []string{"C"}) 28 | assert.Equal(1, len(m.fields)) 29 | } 30 | 31 | func TestInterfaceToModelWithReference(t *testing.T) { 32 | assert := NewAssert(t) 33 | type parent struct { 34 | Id int64 35 | Name string 36 | Value string 37 | } 38 | type table struct { 39 | ColPrimary int64 `qbs:"pk"` 40 | FatherId int64 `qbs:"fk:Father"` 41 | Father *parent 42 | } 43 | table1 := &table{ 44 | 6, 3, &parent{3, "Mrs. A", "infinite"}, 45 | } 46 | m := structPtrToModel(table1, true, nil) 47 | ref, ok := m.refs["Father"] 48 | assert.MustTrue(ok) 49 | f := ref.model.fields[1] 50 | x, ok := f.value.(string) 51 | assert.True(ok) 52 | assert.Equal("Mrs. A", x) 53 | } 54 | 55 | type indexedTable struct { 56 | ColPrimary int64 `qbs:"pk"` 57 | ColNotNull string `qbs:"notnull,default:'banana'"` 58 | ColVarChar string `qbs:"size:64"` 59 | ColTime time.Time 60 | } 61 | 62 | func (table *indexedTable) Indexes(indexes *Indexes) { 63 | indexes.Add("col_primary", "col_time") 64 | indexes.AddUnique("col_var_char", "col_time") 65 | } 66 | 67 | func TestInterfaceToModel(t *testing.T) { 68 | assert := NewAssert(t) 69 | now := time.Now() 70 | table1 := &indexedTable{ 71 | ColPrimary: 6, 72 | ColVarChar: "orange", 73 | ColTime: now, 74 | } 75 | m := structPtrToModel(table1, true, nil) 76 | assert.Equal("col_primary", m.pk.name) 77 | assert.Equal(4, len(m.fields)) 78 | assert.Equal(2, len(m.indexes)) 79 | assert.Equal("col_primary_col_time", m.indexes[0].name) 80 | assert.True(!m.indexes[0].unique) 81 | assert.Equal("col_var_char_col_time", m.indexes[1].name) 82 | assert.True(m.indexes[1].unique) 83 | 84 | f := m.fields[0] 85 | assert.Equal(6, f.value) 86 | assert.True(f.pk) 87 | 88 | f = m.fields[1] 89 | assert.Equal("'banana'", f.dfault) 90 | 91 | f = m.fields[2] 92 | str, _ := f.value.(string) 93 | assert.Equal("orange", str) 94 | assert.Equal(64, f.size) 95 | 96 | f = m.fields[3] 97 | tm, _ := f.value.(time.Time) 98 | assert.Equal(now, tm) 99 | } 100 | 101 | func TestInterfaceToSubModel(t *testing.T) { 102 | assert := NewAssert(t) 103 | type User struct { 104 | Id int64 105 | Name string 106 | } 107 | type Post struct { 108 | Id int64 109 | AuthorId int64 `qbs:"fk:Author"` 110 | Author *User 111 | Content string 112 | unexported int64 113 | } 114 | pst := new(Post) 115 | model := structPtrToModel(pst, true, nil) 116 | assert.Equal(1, len(model.refs)) 117 | } 118 | 119 | func TestColumnsAndValues(t *testing.T) { 120 | assert := NewAssert(t) 121 | type User struct { 122 | Id int64 123 | Name string 124 | } 125 | user := new(User) 126 | model := structPtrToModel(user, true, nil) 127 | columns, values := model.columnsAndValues(false) 128 | assert.MustEqual(1, len(columns)) 129 | assert.MustEqual(1, len(values)) 130 | } 131 | 132 | type SomethingNotUser struct { 133 | Id int64 134 | Name string 135 | } 136 | 137 | func (s *SomethingNotUser) TableName() string { 138 | return "User" 139 | } 140 | 141 | func TestTableNamer(t *testing.T) { 142 | assert := NewAssert(t) 143 | var u SomethingNotUser 144 | model := structPtrToModel(&u, true, nil) 145 | assert.Equal("User", model.table) 146 | } 147 | 148 | type PointerStructFields struct { 149 | Id uint64 150 | StrPointer *string 151 | Str string 152 | Ignored *PointerStructFields 153 | } 154 | 155 | func TestModelFieldsIndicateNullable(t *testing.T) { 156 | assert := NewAssert(t) 157 | var p PointerStructFields 158 | model := structPtrToModel(&p, true, nil) 159 | assert.Equal(3, len(model.fields)) 160 | for i := 0; i < 3; i++ { 161 | f := model.fields[i] 162 | if f.name == "str_pointer" { 163 | assert.Equal(f.nullable, reflect.String) 164 | } else { 165 | assert.Equal(f.nullable, reflect.Invalid) 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /syntax_test_util.go: -------------------------------------------------------------------------------- 1 | package qbs 2 | 3 | type dialectSyntax struct { 4 | dialect Dialect 5 | createTableWithoutPkIfExistsSql string 6 | createTableWithPkSql string 7 | insertSql string 8 | updateSql string 9 | deleteSql string 10 | selectionSql string 11 | querySql string 12 | dropTableIfExistsSql string 13 | addColumnSql string 14 | createUniqueIndexSql string 15 | createIndexSql string 16 | } 17 | 18 | type sqlGenModel struct { 19 | Prim int64 `qbs:"pk"` 20 | First string 21 | Last string 22 | Amount int 23 | } 24 | 25 | var sqlGenSampleData = &sqlGenModel{3, "FirstName", "LastName", 6} 26 | 27 | type addColumnTestTable struct { 28 | Newc string `qbs:"size:100"` 29 | } 30 | 31 | func doTestAddColumSQL(assert *Assert, info dialectSyntax) { 32 | testModel := structPtrToModel(new(addColumnTestTable), false, nil) 33 | sql := info.dialect.addColumnSql("a", *testModel.fields[0]) 34 | assert.Equal(info.addColumnSql, sql) 35 | } 36 | 37 | func doTestCreateTableSQL(assert *Assert, info dialectSyntax) { 38 | type withoutPk struct { 39 | First string 40 | Last string 41 | Amount int 42 | } 43 | table := &withoutPk{"a", "b", 5} 44 | model := structPtrToModel(table, true, nil) 45 | sql := info.dialect.createTableSql(model, true) 46 | assert.Equal(info.createTableWithoutPkIfExistsSql, sql) 47 | type withPk struct { 48 | Primary int64 `qbs:"pk"` 49 | First string 50 | Last string 51 | Amount int 52 | } 53 | table2 := &withPk{First: "a", Last: "b", Amount: 5} 54 | model = structPtrToModel(table2, true, nil) 55 | sql = info.dialect.createTableSql(model, false) 56 | assert.Equal(info.createTableWithPkSql, sql) 57 | } 58 | 59 | func doTestCreateIndexSQL(assert *Assert, info dialectSyntax) { 60 | sql := info.dialect.createIndexSql("iname", "itable", true, "a", "b", "c") 61 | assert.Equal(info.createUniqueIndexSql, sql) 62 | sql = info.dialect.createIndexSql("iname2", "itable2", false, "d", "e") 63 | assert.Equal(info.createIndexSql, sql) 64 | } 65 | 66 | func doTestInsertSQL(assert *Assert, info dialectSyntax) { 67 | model := structPtrToModel(sqlGenSampleData, true, nil) 68 | criteria := &criteria{model: model} 69 | criteria.mergePkCondition(info.dialect) 70 | sql, _ := info.dialect.insertSql(criteria) 71 | sql = info.dialect.substituteMarkers(sql) 72 | assert.Equal(info.insertSql, sql) 73 | } 74 | 75 | func doTestUpdateSQL(assert *Assert, info dialectSyntax) { 76 | model := structPtrToModel(sqlGenSampleData, true, nil) 77 | criteria := &criteria{model: model} 78 | criteria.mergePkCondition(info.dialect) 79 | sql, _ := info.dialect.updateSql(criteria) 80 | sql = info.dialect.substituteMarkers(sql) 81 | assert.Equal(info.updateSql, sql) 82 | } 83 | 84 | func doTestDeleteSQL(assert *Assert, info dialectSyntax) { 85 | model := structPtrToModel(sqlGenSampleData, true, nil) 86 | criteria := &criteria{model: model} 87 | criteria.mergePkCondition(info.dialect) 88 | sql, _ := info.dialect.deleteSql(criteria) 89 | sql = info.dialect.substituteMarkers(sql) 90 | assert.Equal(info.deleteSql, sql) 91 | } 92 | 93 | func doTestSelectionSQL(assert *Assert, info dialectSyntax) { 94 | type User struct { 95 | Id int64 96 | Name string 97 | } 98 | type Post struct { 99 | Id int64 100 | AuthorId int64 `qbs:"fk:Author"` 101 | Author *User 102 | Content string 103 | } 104 | model := structPtrToModel(new(Post), true, nil) 105 | criteria := new(criteria) 106 | criteria.model = model 107 | 108 | sql, _ := info.dialect.querySql(criteria) 109 | assert.Equal(info.selectionSql, sql) 110 | } 111 | 112 | func doTestQuerySQL(assert *Assert, info dialectSyntax) { 113 | type Student struct { 114 | Name string 115 | Grade int 116 | Score int 117 | } 118 | model := structPtrToModel(new(Student), true, nil) 119 | criteria := new(criteria) 120 | criteria.model = model 121 | condition := NewInCondition("grade", []interface{}{6, 7, 8}) 122 | subCondition := NewCondition("score <= ?", 60).Or("score >= ?", 80) 123 | condition.AndCondition(subCondition) 124 | criteria.condition = condition 125 | criteria.orderBys = []order{order{info.dialect.quote("name"), false}, order{info.dialect.quote("grade"), true}} 126 | criteria.offset = 3 127 | criteria.limit = 10 128 | sql, _ := info.dialect.querySql(criteria) 129 | sql = info.dialect.substituteMarkers(sql) 130 | assert.Equal(info.querySql, sql) 131 | } 132 | 133 | func doTestDropTableSQL(assert *Assert, info dialectSyntax) { 134 | sql := info.dialect.dropTableSql("drop_table") 135 | assert.Equal(info.dropTableIfExistsSql, sql) 136 | } 137 | -------------------------------------------------------------------------------- /postgres.go: -------------------------------------------------------------------------------- 1 | package qbs 2 | 3 | import ( 4 | "bytes" 5 | "database/sql" 6 | "fmt" 7 | "reflect" 8 | "strconv" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | type postgres struct { 14 | base 15 | } 16 | 17 | func NewPostgres() Dialect { 18 | d := new(postgres) 19 | d.base.dialect = d 20 | return d 21 | } 22 | 23 | func DefaultPostgresDataSourceName(dbName string) *DataSourceName { 24 | dsn := new(DataSourceName) 25 | dsn.Dialect = NewPostgres() 26 | dsn.Username = "postgres" 27 | dsn.DbName = dbName 28 | dsn.Append("sslmode", "disable") 29 | return dsn 30 | } 31 | 32 | func (d postgres) quote(s string) string { 33 | segs := strings.Split(s, ".") 34 | buf := new(bytes.Buffer) 35 | buf.WriteByte('"') 36 | buf.WriteString(segs[0]) 37 | for i := 1; i < len(segs); i++ { 38 | buf.WriteString(`"."`) 39 | buf.WriteString(segs[i]) 40 | } 41 | buf.WriteByte('"') 42 | return buf.String() 43 | } 44 | 45 | func (d postgres) sqlType(field modelField) string { 46 | f := field.value 47 | fieldValue := reflect.ValueOf(f) 48 | kind := fieldValue.Kind() 49 | if field.nullable != reflect.Invalid { 50 | kind = field.nullable 51 | } 52 | switch kind { 53 | case reflect.Bool: 54 | return "boolean" 55 | case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint8, reflect.Uint16, reflect.Uint32: 56 | return "integer" 57 | case reflect.Uint, reflect.Uint64, reflect.Int, reflect.Int64: 58 | return "bigint" 59 | case reflect.Float32, reflect.Float64: 60 | return "double precision" 61 | case reflect.String: 62 | if field.size > 0 && field.size < 65532 { 63 | return fmt.Sprintf("varchar(%d)", field.size) 64 | } 65 | return "text" 66 | case reflect.Slice: 67 | if reflect.TypeOf(f).Elem().Kind() == reflect.Uint8 { 68 | if field.size > 0 && field.size < 65532 { 69 | return fmt.Sprintf("varbinary(%d)", field.size) 70 | } 71 | return "bytea" 72 | } 73 | case reflect.Struct: 74 | switch fieldValue.Interface().(type) { 75 | case time.Time: 76 | return "timestamp with time zone" 77 | case sql.NullBool: 78 | return "boolean" 79 | case sql.NullInt64: 80 | return "bigint" 81 | case sql.NullFloat64: 82 | return "double precision" 83 | case sql.NullString: 84 | if field.size > 0 && field.size < 65532 { 85 | return fmt.Sprintf("varchar(%d)", field.size) 86 | } 87 | return "text" 88 | default: 89 | if len(field.colType) != 0 { 90 | switch field.colType { 91 | case QBS_COLTYPE_BOOL, QBS_COLTYPE_BIGINT: 92 | return field.colType 93 | case QBS_COLTYPE_INT: 94 | return "integer" 95 | case QBS_COLTYPE_DOUBLE: 96 | return "double precision" 97 | case QBS_COLTYPE_TIME: 98 | return "timestamp with time zone" 99 | case QBS_COLTYPE_TEXT: 100 | if field.size > 0 && field.size < 65532 { 101 | return fmt.Sprintf("varchar(%d)", field.size) 102 | } 103 | return "text" 104 | default: 105 | panic("Qbs doesn't support column type " + 106 | field.colType + " for postgres") 107 | } 108 | } 109 | } 110 | } 111 | panic("invalid sql type for field:" + field.name) 112 | } 113 | 114 | func (d postgres) insert(q *Qbs) (int64, error) { 115 | sql, args := d.dialect.insertSql(q.criteria) 116 | row := q.QueryRow(sql, args...) 117 | value := q.criteria.model.pk.value 118 | var err error 119 | var id int64 120 | if _, ok := value.(int64); ok { 121 | err = row.Scan(&id) 122 | } else if _, ok := value.(string); ok { 123 | var str string 124 | err = row.Scan(&str) 125 | } 126 | return id, err 127 | } 128 | 129 | func (d postgres) insertSql(criteria *criteria) (string, []interface{}) { 130 | sql, values := d.base.insertSql(criteria) 131 | sql += " RETURNING " + d.dialect.quote(criteria.model.pk.name) 132 | return sql, values 133 | } 134 | 135 | func (d postgres) indexExists(mg *Migration, tableName, indexName string) bool { 136 | var row *sql.Row 137 | var name string 138 | query := "SELECT indexname FROM pg_indexes " 139 | query += "WHERE tablename = ? AND indexname = ?" 140 | query = d.substituteMarkers(query) 141 | row = mg.db.QueryRow(query, tableName, indexName) 142 | row.Scan(&name) 143 | return name != "" 144 | } 145 | 146 | func (d postgres) substituteMarkers(query string) string { 147 | position := 1 148 | buf := new(bytes.Buffer) 149 | for i := 0; i < len(query); i++ { 150 | c := query[i] 151 | if c == '?' { 152 | buf.WriteByte('$') 153 | buf.WriteString(strconv.Itoa(position)) 154 | position++ 155 | } else { 156 | buf.WriteByte(c) 157 | } 158 | } 159 | return buf.String() 160 | } 161 | 162 | func (d postgres) columnsInTable(mg *Migration, table interface{}) map[string]bool { 163 | tn := tableName(table) 164 | columns := make(map[string]bool) 165 | query := "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ?" 166 | query = mg.dialect.substituteMarkers(query) 167 | rows, err := mg.db.Query(query, tn) 168 | defer rows.Close() 169 | if err != nil { 170 | panic(err) 171 | } 172 | for rows.Next() { 173 | column := "" 174 | err := rows.Scan(&column) 175 | if err == nil { 176 | columns[column] = true 177 | } 178 | } 179 | return columns 180 | } 181 | 182 | func (d postgres) primaryKeySql(isString bool, size int) string { 183 | if isString { 184 | return "text PRIMARY KEY" 185 | } 186 | return "bigserial PRIMARY KEY" 187 | } 188 | -------------------------------------------------------------------------------- /oracle.go: -------------------------------------------------------------------------------- 1 | package qbs 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | type oracle struct { 11 | base 12 | } 13 | 14 | func NewOracle() Dialect { 15 | d := &oracle{} 16 | d.base.dialect = d 17 | return d 18 | } 19 | 20 | func (d oracle) quote(s string) string { 21 | sep := "." 22 | a := []string{} 23 | c := strings.Split(s, sep) 24 | for _, v := range c { 25 | a = append(a, fmt.Sprintf(`"%s"`, v)) 26 | } 27 | return strings.Join(a, sep) 28 | } 29 | 30 | func (d oracle) sqlType(field modelField) string { 31 | f := field.value 32 | switch f.(type) { 33 | case time.Time: 34 | return "DATE" 35 | /* 36 | case bool: 37 | return "boolean" 38 | */ 39 | case int, int8, int16, int32, uint, uint8, uint16, uint32, int64, uint64: 40 | if field.size > 0 { 41 | return fmt.Sprintf("NUMBER(%d)", field.size) 42 | } 43 | return "NUMBER" 44 | case float32, float64: 45 | if field.size > 0 { 46 | return fmt.Sprintf("NUMBER(%d,%d)", field.size/10, field.size%10) 47 | } 48 | return "NUMBER(16,2)" 49 | case []byte, string: 50 | if field.size > 0 && field.size < 4000 { 51 | return fmt.Sprintf("VARCHAR2(%d)", field.size) 52 | } 53 | return "CLOB" 54 | default: 55 | if len(field.colType) != 0 { 56 | switch field.colType { 57 | case QBS_COLTYPE_BOOL: 58 | panic("Qbs doesn't support column type " + field.colType + "for Oracle") 59 | case QBS_COLTYPE_INT, QBS_COLTYPE_BIGINT: 60 | return "NUMBER" 61 | case QBS_COLTYPE_DOUBLE: 62 | if field.size > 0 { 63 | return fmt.Sprintf("NUMBER(%d,%d)", field.size/10, field.size%10) 64 | } 65 | return "NUMBER(16,2)" 66 | case QBS_COLTYPE_TIME: 67 | return "DATE" 68 | case QBS_COLTYPE_TEXT: 69 | if field.size > 0 && field.size < 4000 { 70 | return fmt.Sprintf("VARCHAR2(%d)", field.size) 71 | } 72 | return "CLOB" 73 | default: 74 | panic("Qbs doesn't support column type " + field.colType + "for Oracle") 75 | } 76 | } 77 | } 78 | panic("invalid sql type for field:" + field.name) 79 | } 80 | 81 | func (d oracle) insert(q *Qbs) (int64, error) { 82 | sql, args := d.dialect.insertSql(q.criteria) 83 | row := q.QueryRow(sql, args...) 84 | value := q.criteria.model.pk.value 85 | var err error 86 | var id int64 87 | if _, ok := value.(int64); ok { 88 | err = row.Scan(&id) 89 | } else if _, ok := value.(string); ok { 90 | var str string 91 | err = row.Scan(&str) 92 | } 93 | return id, err 94 | } 95 | 96 | func (d oracle) insertSql(criteria *criteria) (string, []interface{}) { 97 | sql, values := d.base.insertSql(criteria) 98 | sql += " RETURNING " + d.dialect.quote(criteria.model.pk.name) 99 | return sql, values 100 | } 101 | 102 | func (d oracle) indexExists(mg *Migration, tableName, indexName string) bool { 103 | var row *sql.Row 104 | var name string 105 | query := "SELECT INDEX_NAME FROM USER_INDEXES " 106 | query += "WHERE TABLE_NAME = ? AND INDEX_NAME = ?" 107 | query = d.substituteMarkers(query) 108 | row = mg.db.QueryRow(query, tableName, indexName) 109 | row.Scan(&name) 110 | return name != "" 111 | } 112 | 113 | func (d oracle) substituteMarkers(query string) string { 114 | position := 1 115 | chunks := make([]string, 0, len(query)*2) 116 | for _, v := range query { 117 | if v == '?' { 118 | chunks = append(chunks, fmt.Sprintf("$%d", position)) 119 | position++ 120 | } else { 121 | chunks = append(chunks, string(v)) 122 | } 123 | } 124 | return strings.Join(chunks, "") 125 | } 126 | 127 | func (d oracle) columnsInTable(mg *Migration, table interface{}) map[string]bool { 128 | tn := tableName(table) 129 | columns := make(map[string]bool) 130 | query := "SELECT COLUMN_NAME FROM USER_TAB_COLUMNS WHERE TABLE_NAME = ?" 131 | query = mg.dialect.substituteMarkers(query) 132 | rows, err := mg.db.Query(query, tn) 133 | defer rows.Close() 134 | if err != nil { 135 | panic(err) 136 | } 137 | for rows.Next() { 138 | column := "" 139 | err := rows.Scan(&column) 140 | if err == nil { 141 | columns[column] = true 142 | } 143 | } 144 | return columns 145 | } 146 | 147 | func (d oracle) primaryKeySql(isString bool, size int) string { 148 | if isString { 149 | return fmt.Sprintf("VARCHAR2(%d) PRIMARY KEY NOT NULL", size) 150 | } 151 | if size == 0 { 152 | size = 16 153 | } 154 | return fmt.Sprintf("NUMBER(%d) PRIMARY KEY NOT NULL", size) 155 | } 156 | 157 | func (d oracle) createTableSql(model *model, ifNotExists bool) string { 158 | baseSql := d.base.createTableSql(model, false) 159 | if _, isString := model.pk.value.(string); isString { 160 | return baseSql 161 | } 162 | table_pk := model.table + "_" + model.pk.name 163 | sequence := " CREATE SEQUENCE " + table_pk + "_seq" + 164 | " MINVALUE 1 NOMAXVALUE START WITH 1 INCREMENT BY 1 NOCACHE CYCLE" 165 | trigger := " CREATE TRIGGER " + table_pk + "_triger BEFORE INSERT ON " + table_pk + 166 | " FOR EACH ROW WHEN (new.id is null)" + 167 | " begin" + 168 | " select " + table_pk + "_seq.nextval into: new.id from dual " + 169 | " end " 170 | return baseSql + ";" + sequence + ";" + trigger 171 | } 172 | 173 | func (d oracle) catchMigrationError(err error) bool { 174 | errString := err.Error() 175 | return strings.Contains(errString, "ORA-00955") || strings.Contains(errString, "ORA-00942") 176 | } 177 | 178 | func (d oracle) dropTableSql(table string) string { 179 | a := []string{"DROP TABLE"} 180 | a = append(a, d.dialect.quote(table)) 181 | return strings.Join(a, " ") 182 | } 183 | -------------------------------------------------------------------------------- /sqlite3.go: -------------------------------------------------------------------------------- 1 | package qbs 2 | 3 | import ( 4 | "database/sql" 5 | "reflect" 6 | "time" 7 | "unsafe" 8 | ) 9 | 10 | type sqlite3 struct { 11 | base 12 | } 13 | 14 | func NewSqlite3() Dialect { 15 | d := new(sqlite3) 16 | d.base.dialect = d 17 | return d 18 | } 19 | 20 | func RegisterSqlite3(dbFileName string) { 21 | dsn := new(DataSourceName) 22 | dsn.DbName = dbFileName 23 | dsn.Dialect = NewSqlite3() 24 | RegisterWithDataSourceName(dsn) 25 | } 26 | 27 | func (d sqlite3) sqlType(field modelField) string { 28 | f := field.value 29 | fieldValue := reflect.ValueOf(f) 30 | kind := fieldValue.Kind() 31 | if field.nullable != reflect.Invalid { 32 | kind = field.nullable 33 | } 34 | switch kind { 35 | case reflect.Bool: 36 | return "integer" 37 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 38 | return "integer" 39 | case reflect.Float32, reflect.Float64: 40 | return "real" 41 | case reflect.String: 42 | return "text" 43 | case reflect.Slice: 44 | if reflect.TypeOf(f).Elem().Kind() == reflect.Uint8 { 45 | return "text" 46 | } 47 | case reflect.Struct: 48 | switch fieldValue.Interface().(type) { 49 | case time.Time: 50 | return "text" 51 | case sql.NullBool: 52 | return "integer" 53 | case sql.NullInt64: 54 | return "integer" 55 | case sql.NullFloat64: 56 | return "real" 57 | case sql.NullString: 58 | return "text" 59 | default: 60 | if len(field.colType) != 0 { 61 | switch field.colType { 62 | case QBS_COLTYPE_INT: 63 | return "integer" 64 | case QBS_COLTYPE_BIGINT: 65 | return "integer" 66 | case QBS_COLTYPE_BOOL: 67 | return "integer" 68 | case QBS_COLTYPE_TIME: 69 | return "text" 70 | case QBS_COLTYPE_DOUBLE: 71 | return "real" 72 | case QBS_COLTYPE_TEXT: 73 | return "text" 74 | default: 75 | panic("Qbs doesn't support column type " + field.colType + "for SQLite3") 76 | } 77 | } 78 | } 79 | } 80 | panic("invalid sql type for field:" + field.name) 81 | } 82 | 83 | func (d sqlite3) setModelValue(value reflect.Value, field reflect.Value) error { 84 | switch field.Type().Kind() { 85 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 86 | field.SetInt(value.Elem().Int()) 87 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 88 | // reading uint from int value causes panic 89 | switch value.Elem().Kind() { 90 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 91 | field.SetUint(uint64(value.Elem().Int())) 92 | default: 93 | field.SetUint(value.Elem().Uint()) 94 | } 95 | case reflect.Bool: 96 | if value.Elem().Int() == 0 { 97 | field.SetBool(false) 98 | } else { 99 | field.SetBool(true) 100 | } 101 | case reflect.Float32, reflect.Float64: 102 | field.SetFloat(value.Elem().Float()) 103 | case reflect.String: 104 | if value.Elem().Kind() == reflect.Slice { 105 | field.SetString(string(value.Elem().Bytes())) 106 | } else { 107 | field.SetString(value.Elem().String()) 108 | } 109 | case reflect.Slice: 110 | if reflect.TypeOf(value.Interface()).Elem().Kind() == reflect.Uint8 { 111 | field.SetBytes(value.Elem().Bytes()) 112 | } 113 | case reflect.Ptr: 114 | d.setPtrValue(value, field) 115 | case reflect.Struct: 116 | switch field.Interface().(type) { 117 | case time.Time: 118 | var t time.Time 119 | var err error 120 | switch value.Elem().Kind() { 121 | case reflect.String: 122 | t, err = time.Parse("2006-01-02 15:04:05", value.Elem().String()) 123 | if err != nil { 124 | return err 125 | } 126 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 127 | t = time.Unix(value.Elem().Int(), 0) 128 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 129 | t = time.Unix(int64(value.Elem().Uint()), 0) 130 | case reflect.Slice: 131 | t, err = time.Parse("2006-01-02 15:04:05", string(value.Elem().Bytes())) 132 | if err != nil { 133 | return err 134 | } 135 | } 136 | v := reflect.NewAt(reflect.TypeOf(time.Time{}), unsafe.Pointer(&t)) 137 | field.Set(v.Elem()) 138 | case sql.NullBool: 139 | b := true 140 | if value.Elem().Int() == 0 { 141 | b = false 142 | } 143 | field.Set(reflect.ValueOf(sql.NullBool{b, true})) 144 | case sql.NullFloat64: 145 | if f, ok := value.Elem().Interface().(float64); ok { 146 | field.Set(reflect.ValueOf(sql.NullFloat64{f, true})) 147 | } 148 | case sql.NullInt64: 149 | if i, ok := value.Elem().Interface().(int64); ok { 150 | field.Set(reflect.ValueOf(sql.NullInt64{i, true})) 151 | } 152 | case sql.NullString: 153 | str := string(value.Elem().String()) 154 | field.Set(reflect.ValueOf(sql.NullString{str, true})) 155 | } 156 | } 157 | return nil 158 | } 159 | 160 | func (d sqlite3) indexExists(mg *Migration, tableName string, indexName string) bool { 161 | query := "PRAGMA index_list('" + tableName + "')" 162 | rows, err := mg.db.Query(query) 163 | if err != nil { 164 | panic(err) 165 | } 166 | defer rows.Close() 167 | for rows.Next() { 168 | var c0, c1, c2 string 169 | rows.Scan(&c0, &c1, &c2) 170 | if c1 == indexName { 171 | return true 172 | } 173 | } 174 | return false 175 | } 176 | 177 | func (d sqlite3) columnsInTable(mg *Migration, table interface{}) map[string]bool { 178 | tn := tableName(table) 179 | columns := make(map[string]bool) 180 | query := "PRAGMA table_info('" + tn + "')" 181 | rows, err := mg.db.Query(query) 182 | if err != nil { 183 | panic(err) 184 | } 185 | defer rows.Close() 186 | for rows.Next() { 187 | cols, _ := rows.Columns() 188 | containers := make([]interface{}, 0, len(cols)) 189 | for i := 0; i < cap(containers); i++ { 190 | var v interface{} 191 | containers = append(containers, &v) 192 | } 193 | err = rows.Scan(containers...) 194 | value := reflect.Indirect(reflect.ValueOf(containers[1])) 195 | if err == nil { 196 | if value.Elem().Kind() == reflect.Slice { 197 | columns[string(value.Elem().Bytes())] = true 198 | } else { 199 | columns[value.Elem().String()] = true 200 | } 201 | } 202 | } 203 | return columns 204 | } 205 | 206 | func (d sqlite3) primaryKeySql(isString bool, size int) string { 207 | if isString { 208 | return "text PRIMARY KEY NOT NULL" 209 | } 210 | return "integer PRIMARY KEY AUTOINCREMENT NOT NULL" 211 | } 212 | -------------------------------------------------------------------------------- /postgres_test.go: -------------------------------------------------------------------------------- 1 | package qbs 2 | 3 | import ( 4 | _ "github.com/lib/pq" 5 | "testing" 6 | //"time" 7 | ) 8 | 9 | var pgSyntax = dialectSyntax{ 10 | NewPostgres(), 11 | `CREATE TABLE IF NOT EXISTS "without_pk" ( "first" text, "last" text, "amount" bigint )`, 12 | `CREATE TABLE "with_pk" ( "primary" bigserial PRIMARY KEY, "first" text, "last" text, "amount" bigint )`, 13 | `INSERT INTO "sql_gen_model" ("prim", "first", "last", "amount") VALUES ($1, $2, $3, $4) RETURNING "prim"`, 14 | `UPDATE "sql_gen_model" SET "first" = $1, "last" = $2, "amount" = $3 WHERE "prim" = $4`, 15 | `DELETE FROM "sql_gen_model" WHERE "prim" = $1`, 16 | `SELECT "post"."id", "post"."author_id", "post"."content", "author"."id" AS author___id, "author"."name" AS author___name FROM "post" LEFT JOIN "user" AS "author" ON "post"."author_id" = "author"."id"`, 17 | `SELECT "name", "grade", "score" FROM "student" WHERE (grade IN ($1, $2, $3)) AND ((score <= $4) OR (score >= $5)) ORDER BY "name", "grade" DESC LIMIT $6 OFFSET $7`, 18 | `DROP TABLE IF EXISTS "drop_table"`, 19 | `ALTER TABLE "a" ADD COLUMN "newc" varchar(100)`, 20 | `CREATE UNIQUE INDEX "iname" ON "itable" ("a", "b", "c")`, 21 | `CREATE INDEX "iname2" ON "itable2" ("d", "e")`, 22 | } 23 | 24 | func registerPgTest() { 25 | RegisterWithDataSourceName(DefaultPostgresDataSourceName(testDbName)) 26 | } 27 | 28 | func setupPgDb() (*Migration, *Qbs) { 29 | registerPgTest() 30 | mg, _ := GetMigration() 31 | q, _ := GetQbs() 32 | return mg, q 33 | } 34 | 35 | var postgresSqlTypeResults []string = []string{ 36 | "boolean", 37 | "integer", 38 | "integer", 39 | "integer", 40 | "integer", 41 | "integer", 42 | "integer", 43 | "bigint", 44 | "bigint", 45 | "bigint", 46 | "bigint", 47 | "double precision", 48 | "double precision", 49 | "varchar(128)", 50 | "text", 51 | "timestamp with time zone", 52 | "bytea", 53 | "bigint", 54 | "integer", 55 | "boolean", 56 | "double precision", 57 | "timestamp with time zone", 58 | "varchar(128)", 59 | "text", 60 | } 61 | 62 | func TestSqlTypeForPgDialect(t *testing.T) { 63 | assert := NewAssert(t) 64 | d := NewPostgres() 65 | testModel := structPtrToModel(new(typeTestTable), false, nil) 66 | for index, column := range testModel.fields { 67 | if storedResult := postgresSqlTypeResults[index]; storedResult != "-" { 68 | result := d.sqlType(*column) 69 | assert.Equal(storedResult, result) 70 | } 71 | } 72 | } 73 | 74 | func TestPgTransaction(t *testing.T) { 75 | registerPgTest() 76 | doTestTransaction(NewAssert(t)) 77 | } 78 | 79 | func TestPgSaveAndDelete(t *testing.T) { 80 | mg, q := setupPgDb() 81 | doTestSaveAndDelete(NewAssert(t), mg, q) 82 | } 83 | 84 | func TestPgSaveAgain(t *testing.T) { 85 | mg, q := setupPgDb() 86 | doTestSaveAgain(NewAssert(t), mg, q) 87 | } 88 | 89 | func TestPgForeignKey(t *testing.T) { 90 | registerPgTest() 91 | doTestForeignKey(NewAssert(t)) 92 | } 93 | 94 | func TestPgFind(t *testing.T) { 95 | registerPgTest() 96 | doTestFind(NewAssert(t)) 97 | } 98 | 99 | func TestPgCreateTable(t *testing.T) { 100 | mg, _ := setupPgDb() 101 | doTestCreateTable(NewAssert(t), mg) 102 | } 103 | 104 | func TestPgUpdate(t *testing.T) { 105 | mg, q := setupPgDb() 106 | doTestUpdate(NewAssert(t), mg, q) 107 | } 108 | 109 | func TestPgValidation(t *testing.T) { 110 | mg, q := setupPgDb() 111 | doTestValidation(NewAssert(t), mg, q) 112 | } 113 | 114 | func TestPgBoolType(t *testing.T) { 115 | mg, q := setupPgDb() 116 | doTestBoolType(NewAssert(t), mg, q) 117 | } 118 | 119 | func TestPgStringPk(t *testing.T) { 120 | mg, q := setupPgDb() 121 | doTestStringPk(NewAssert(t), mg, q) 122 | } 123 | 124 | func TestPgCount(t *testing.T) { 125 | registerPgTest() 126 | doTestCount(NewAssert(t)) 127 | } 128 | 129 | func TestPgQueryMap(t *testing.T) { 130 | mg, q := setupPgDb() 131 | doTestQueryMap(NewAssert(t), mg, q) 132 | } 133 | 134 | func TestPgBulkInsert(t *testing.T) { 135 | registerPgTest() 136 | doTestBulkInsert(NewAssert(t)) 137 | } 138 | 139 | func TestPgQueryStruct(t *testing.T) { 140 | registerPgTest() 141 | doTestQueryStruct(NewAssert(t)) 142 | } 143 | 144 | func TestPgConnectionLimit(t *testing.T) { 145 | registerPgTest() 146 | doTestConnectionLimit(NewAssert(t)) 147 | } 148 | 149 | func TestPgIterate(t *testing.T) { 150 | registerPgTest() 151 | doTestIterate(NewAssert(t)) 152 | } 153 | 154 | func TestPgAddColumnSQL(t *testing.T) { 155 | doTestAddColumSQL(NewAssert(t), pgSyntax) 156 | } 157 | 158 | func TestPgCreateTableSQL(t *testing.T) { 159 | doTestCreateTableSQL(NewAssert(t), pgSyntax) 160 | } 161 | 162 | func TestPgCreateIndexSQL(t *testing.T) { 163 | doTestCreateIndexSQL(NewAssert(t), pgSyntax) 164 | } 165 | 166 | func TestPgInsertSQL(t *testing.T) { 167 | doTestInsertSQL(NewAssert(t), pgSyntax) 168 | } 169 | 170 | func TestPgUpdateSQL(t *testing.T) { 171 | doTestUpdateSQL(NewAssert(t), pgSyntax) 172 | } 173 | 174 | func TestPgDeleteSQL(t *testing.T) { 175 | doTestDeleteSQL(NewAssert(t), pgSyntax) 176 | } 177 | 178 | func TestPgSelectionSQL(t *testing.T) { 179 | doTestSelectionSQL(NewAssert(t), pgSyntax) 180 | } 181 | 182 | func TestPgQuerySQL(t *testing.T) { 183 | doTestQuerySQL(NewAssert(t), pgSyntax) 184 | } 185 | 186 | func TestPgDropTableSQL(t *testing.T) { 187 | doTestDropTableSQL(NewAssert(t), pgSyntax) 188 | } 189 | 190 | func TestPgSaveNullable(t *testing.T) { 191 | mg, q := setupPgDb() 192 | doTestSaveNullable(NewAssert(t), mg, q) 193 | } 194 | 195 | func TestPgDataSourceName(t *testing.T) { 196 | dsn := new(DataSourceName) 197 | dsn.DbName = "abc" 198 | dsn.Username = "john" 199 | dsn.Dialect = NewPostgres() 200 | assert := NewAssert(t) 201 | assert.Equal("user=john dbname=abc", dsn) 202 | dsn.Password = "123" 203 | assert.Equal("user=john password=123 dbname=abc", dsn) 204 | dsn.Host = "192.168.1.3" 205 | assert.Equal("user=john password=123 dbname=abc host=192.168.1.3", dsn) 206 | dsn.UnixSocket = true 207 | assert.Equal("user=john password=123 dbname=abc host=/192.168.1.3", dsn) 208 | dsn.Port = "9876" 209 | assert.Equal("user=john password=123 dbname=abc host=/192.168.1.3 port=9876", dsn) 210 | } 211 | 212 | func BenchmarkPgFind(b *testing.B) { 213 | registerPgTest() 214 | doBenchmarkFind(b, b.N) 215 | } 216 | 217 | func BenchmarkPgDbQuery(b *testing.B) { 218 | registerPgTest() 219 | doBenchmarkDbQuery(b, b.N) 220 | } 221 | 222 | func BenchmarkPgStmtQuery(b *testing.B) { 223 | registerPgTest() 224 | doBenchmarkStmtQuery(b, b.N) 225 | } 226 | 227 | func BenchmarkPgTransaction(b *testing.B) { 228 | registerPgTest() 229 | doBenchmarkTransaction(b, b.N) 230 | } 231 | -------------------------------------------------------------------------------- /mysql_test.go: -------------------------------------------------------------------------------- 1 | package qbs 2 | 3 | import ( 4 | _ "github.com/coocood/mysql" 5 | "testing" 6 | ) 7 | 8 | var mysqlSyntax = dialectSyntax{ 9 | NewMysql(), 10 | "CREATE TABLE IF NOT EXISTS `without_pk` ( `first` longtext, `last` longtext, `amount` bigint )", 11 | "CREATE TABLE `with_pk` ( `primary` bigint PRIMARY KEY AUTO_INCREMENT, `first` longtext, `last` longtext, `amount` bigint )", 12 | "INSERT INTO `sql_gen_model` (`prim`, `first`, `last`, `amount`) VALUES (?, ?, ?, ?)", 13 | "UPDATE `sql_gen_model` SET `first` = ?, `last` = ?, `amount` = ? WHERE `prim` = ?", 14 | "DELETE FROM `sql_gen_model` WHERE `prim` = ?", 15 | "SELECT `post`.`id`, `post`.`author_id`, `post`.`content`, `author`.`id` AS author___id, `author`.`name` AS author___name FROM `post` LEFT JOIN `user` AS `author` ON `post`.`author_id` = `author`.`id`", 16 | "SELECT `name`, `grade`, `score` FROM `student` WHERE (grade IN (?, ?, ?)) AND ((score <= ?) OR (score >= ?)) ORDER BY `name`, `grade` DESC LIMIT ? OFFSET ?", 17 | "DROP TABLE IF EXISTS `drop_table`", 18 | "ALTER TABLE `a` ADD COLUMN `newc` varchar(100)", 19 | "CREATE UNIQUE INDEX `iname` ON `itable` (`a`, `b`, `c`)", 20 | "CREATE INDEX `iname2` ON `itable2` (`d`, `e`)", 21 | } 22 | 23 | func setupMysqlDb() (*Migration, *Qbs) { 24 | registerMysqlTest() 25 | mg, _ := GetMigration() 26 | q, _ := GetQbs() 27 | return mg, q 28 | } 29 | 30 | func registerMysqlTest() { 31 | dsn := new(DataSourceName) 32 | dsn.DbName = testDbName 33 | dsn.Username = "root" 34 | dsn.Dialect = NewMysql() 35 | dsn.Append("parseTime", "true").Append("loc", "Local") 36 | RegisterWithDataSourceName(dsn) 37 | } 38 | 39 | var mysqlSqlTypeResults []string = []string{ 40 | "boolean", 41 | "int", 42 | "int", 43 | "int", 44 | "int", 45 | "int", 46 | "int", 47 | "bigint", 48 | "bigint", 49 | "bigint", 50 | "bigint", 51 | "double", 52 | "double", 53 | "varchar(128)", 54 | "longtext", 55 | "timestamp", 56 | "longblob", 57 | "bigint", 58 | "int", 59 | "boolean", 60 | "double", 61 | "timestamp", 62 | "varchar(128)", 63 | "longtext", 64 | } 65 | 66 | func TestMysqlSqlType(t *testing.T) { 67 | assert := NewAssert(t) 68 | 69 | d := NewMysql() 70 | testModel := structPtrToModel(new(typeTestTable), false, nil) 71 | for index, column := range testModel.fields { 72 | if storedResult := mysqlSqlTypeResults[index]; storedResult != "-" { 73 | result := d.sqlType(*column) 74 | assert.Equal(storedResult, result) 75 | } 76 | } 77 | } 78 | 79 | func TestMysqlTransaction(t *testing.T) { 80 | registerMysqlTest() 81 | doTestTransaction(NewAssert(t)) 82 | } 83 | 84 | func TestMysqlSaveAndDelete(t *testing.T) { 85 | mg, q := setupMysqlDb() 86 | doTestSaveAndDelete(NewAssert(t), mg, q) 87 | } 88 | 89 | func TestMysqlSaveAgain(t *testing.T) { 90 | mg, q := setupMysqlDb() 91 | doTestSaveAgain(NewAssert(t), mg, q) 92 | } 93 | 94 | func TestMysqlForeignKey(t *testing.T) { 95 | registerMysqlTest() 96 | doTestForeignKey(NewAssert(t)) 97 | } 98 | 99 | func TestMysqlFind(t *testing.T) { 100 | registerMysqlTest() 101 | doTestFind(NewAssert(t)) 102 | } 103 | 104 | func TestMysqlCreateTable(t *testing.T) { 105 | mg, _ := setupMysqlDb() 106 | doTestCreateTable(NewAssert(t), mg) 107 | } 108 | 109 | func TestMysqlUpdate(t *testing.T) { 110 | mg, q := setupMysqlDb() 111 | doTestUpdate(NewAssert(t), mg, q) 112 | } 113 | 114 | func TestMysqlValidation(t *testing.T) { 115 | mg, q := setupMysqlDb() 116 | doTestValidation(NewAssert(t), mg, q) 117 | } 118 | 119 | func TestMysqlBoolType(t *testing.T) { 120 | mg, q := setupMysqlDb() 121 | doTestBoolType(NewAssert(t), mg, q) 122 | } 123 | 124 | func TestMysqlStringPk(t *testing.T) { 125 | mg, q := setupMysqlDb() 126 | doTestStringPk(NewAssert(t), mg, q) 127 | } 128 | 129 | func TestMysqlCount(t *testing.T) { 130 | registerMysqlTest() 131 | doTestCount(NewAssert(t)) 132 | } 133 | 134 | func TestMysqlQueryMap(t *testing.T) { 135 | mg, q := setupMysqlDb() 136 | doTestQueryMap(NewAssert(t), mg, q) 137 | } 138 | 139 | func TestMysqlBulkInsert(t *testing.T) { 140 | registerMysqlTest() 141 | doTestBulkInsert(NewAssert(t)) 142 | } 143 | 144 | func TestMysqlQueryStruct(t *testing.T) { 145 | registerMysqlTest() 146 | doTestQueryStruct(NewAssert(t)) 147 | } 148 | 149 | func TestMysqlCustomNameConvertion(t *testing.T) { 150 | registerMysqlTest() 151 | ColumnNameToFieldName = noConvert 152 | FieldNameToColumnName = noConvert 153 | TableNameToStructName = noConvert 154 | StructNameToTableName = noConvert 155 | doTestForeignKey(NewAssert(t)) 156 | ColumnNameToFieldName = snakeToUpperCamel 157 | FieldNameToColumnName = toSnake 158 | TableNameToStructName = snakeToUpperCamel 159 | StructNameToTableName = toSnake 160 | } 161 | 162 | func TestMysqlConnectionLimit(t *testing.T) { 163 | registerMysqlTest() 164 | doTestConnectionLimit(NewAssert(t)) 165 | } 166 | 167 | func TestMysqlIterate(t *testing.T) { 168 | registerMysqlTest() 169 | doTestIterate(NewAssert(t)) 170 | } 171 | 172 | func TestMysqlAddColumnSQL(t *testing.T) { 173 | doTestAddColumSQL(NewAssert(t), mysqlSyntax) 174 | } 175 | 176 | func TestMysqlCreateTableSQL(t *testing.T) { 177 | doTestCreateTableSQL(NewAssert(t), mysqlSyntax) 178 | } 179 | 180 | func TestMysqlCreateIndexSQL(t *testing.T) { 181 | doTestCreateIndexSQL(NewAssert(t), mysqlSyntax) 182 | } 183 | 184 | func TestMysqlInsertSQL(t *testing.T) { 185 | doTestInsertSQL(NewAssert(t), mysqlSyntax) 186 | } 187 | 188 | func TestMysqlUpdateSQL(t *testing.T) { 189 | doTestUpdateSQL(NewAssert(t), mysqlSyntax) 190 | } 191 | 192 | func TestMysqlDeleteSQL(t *testing.T) { 193 | doTestDeleteSQL(NewAssert(t), mysqlSyntax) 194 | } 195 | 196 | func TestMysqlSelectionSQL(t *testing.T) { 197 | doTestSelectionSQL(NewAssert(t), mysqlSyntax) 198 | } 199 | 200 | func TestMysqlQuerySQL(t *testing.T) { 201 | doTestQuerySQL(NewAssert(t), mysqlSyntax) 202 | } 203 | func TestMysqlDropTableSQL(t *testing.T) { 204 | doTestDropTableSQL(NewAssert(t), mysqlSyntax) 205 | } 206 | 207 | func TestMysqlDataSourceName(t *testing.T) { 208 | dsn := new(DataSourceName) 209 | dsn.DbName = "abc" 210 | dsn.Username = "john" 211 | dsn.Dialect = NewMysql() 212 | assert := NewAssert(t) 213 | assert.Equal("john@/abc", dsn) 214 | dsn.Password = "123" 215 | assert.Equal("john:123@/abc", dsn) 216 | dsn.Host = "192.168.1.3" 217 | assert.Equal("john:123@tcp(192.168.1.3)/abc", dsn) 218 | dsn.UnixSocket = true 219 | assert.Equal("john:123@unix(192.168.1.3)/abc", dsn) 220 | dsn.Append("charset", "utf8") 221 | dsn.Append("parseTime", "true") 222 | assert.Equal("john:123@unix(192.168.1.3)/abc?charset=utf8&parseTime=true", dsn) 223 | dsn.Port = "3336" 224 | assert.Equal("john:123@unix(192.168.1.3:3336)/abc?charset=utf8&parseTime=true", dsn) 225 | } 226 | 227 | func TestMysqlSaveNullable(t *testing.T) { 228 | mg, q := setupMysqlDb() 229 | doTestSaveNullable(NewAssert(t), mg, q) 230 | } 231 | 232 | func BenchmarkMysqlFind(b *testing.B) { 233 | registerMysqlTest() 234 | doBenchmarkFind(b, b.N) 235 | } 236 | 237 | func BenchmarkMysqlQueryStruct(b *testing.B) { 238 | registerMysqlTest() 239 | doBenchmarkQueryStruct(b, b.N) 240 | } 241 | 242 | func BenchmarkMysqlDbQuery(b *testing.B) { 243 | registerMysqlTest() 244 | doBenchmarkDbQuery(b, b.N) 245 | } 246 | 247 | func BenchmarkMysqlStmtQuery(b *testing.B) { 248 | registerMysqlTest() 249 | doBenchmarkStmtQuery(b, b.N) 250 | } 251 | 252 | func BenchmarkMysqlTransaction(b *testing.B) { 253 | registerMysqlTest() 254 | doBenchmarkTransaction(b, b.N) 255 | } 256 | -------------------------------------------------------------------------------- /sqlite3_test.go: -------------------------------------------------------------------------------- 1 | package qbs 2 | 3 | import ( 4 | "testing" 5 | //"time" 6 | 7 | _ "github.com/mattn/go-sqlite3" 8 | ) 9 | 10 | var sqlite3Syntax = dialectSyntax{ 11 | NewSqlite3(), 12 | "CREATE TABLE IF NOT EXISTS `without_pk` ( `first` text, `last` text, `amount` integer )", 13 | "CREATE TABLE `with_pk` ( `primary` integer PRIMARY KEY AUTOINCREMENT NOT NULL, `first` text, `last` text, `amount` integer )", 14 | "INSERT INTO `sql_gen_model` (`prim`, `first`, `last`, `amount`) VALUES (?, ?, ?, ?)", 15 | "UPDATE `sql_gen_model` SET `first` = ?, `last` = ?, `amount` = ? WHERE `prim` = ?", 16 | "DELETE FROM `sql_gen_model` WHERE `prim` = ?", 17 | "SELECT `post`.`id`, `post`.`author_id`, `post`.`content`, `author`.`id` AS author___id, `author`.`name` AS author___name FROM `post` LEFT JOIN `user` AS `author` ON `post`.`author_id` = `author`.`id`", 18 | "SELECT `name`, `grade`, `score` FROM `student` WHERE (grade IN (?, ?, ?)) AND ((score <= ?) OR (score >= ?)) ORDER BY `name`, `grade` DESC LIMIT ? OFFSET ?", 19 | "DROP TABLE IF EXISTS `drop_table`", 20 | "ALTER TABLE `a` ADD COLUMN `newc` text", 21 | "CREATE UNIQUE INDEX `iname` ON `itable` (`a`, `b`, `c`)", 22 | "CREATE INDEX `iname2` ON `itable2` (`d`, `e`)", 23 | } 24 | 25 | func registerSqlite3Test() { 26 | RegisterSqlite3("/tmp/foo.db") 27 | } 28 | 29 | func setupSqlite3Db() (*Migration, *Qbs) { 30 | registerSqlite3Test() 31 | mg, _ := GetMigration() 32 | q, _ := GetQbs() 33 | return mg, q 34 | } 35 | 36 | var sqlite3SqlTypeResults []string = []string{ 37 | "integer", 38 | "integer", 39 | "integer", 40 | "integer", 41 | "integer", 42 | "integer", 43 | "integer", 44 | "integer", 45 | "integer", 46 | "integer", 47 | "integer", 48 | "real", 49 | "real", 50 | "text", 51 | "text", 52 | "text", 53 | "text", 54 | "integer", 55 | "integer", 56 | "integer", 57 | "real", 58 | "text", 59 | "text", 60 | "text", 61 | } 62 | 63 | func TestSqlite3SqlType(t *testing.T) { 64 | assert := NewAssert(t) 65 | d := NewSqlite3() 66 | testModel := structPtrToModel(new(typeTestTable), false, nil) 67 | for index, column := range testModel.fields { 68 | if storedResult := sqlite3SqlTypeResults[index]; storedResult != "-" { 69 | result := d.sqlType(*column) 70 | assert.Equal(storedResult, result) 71 | } 72 | } 73 | /*for _, column := range testModel.fields { 74 | result := d.sqlType(*column) 75 | 76 | switch column.camelName { 77 | case "Bool": 78 | assert.Equal("integer", result) 79 | 80 | case "Int8": 81 | assert.Equal("integer", result) 82 | case "Int16": 83 | assert.Equal("integer", result) 84 | case "Int32": 85 | assert.Equal("integer", result) 86 | case "UInt8": 87 | assert.Equal("integer", result) 88 | case "UInt16": 89 | assert.Equal("integer", result) 90 | case "UInt32": 91 | assert.Equal("integer", result) 92 | 93 | case "Int": 94 | assert.Equal("integer", result) 95 | case "UInt": 96 | assert.Equal("integer", result) 97 | case "Int64": 98 | assert.Equal("integer", result) 99 | case "UInt64": 100 | assert.Equal("integer", result) 101 | 102 | case "Float32": 103 | assert.Equal("real", result) 104 | case "Float64": 105 | assert.Equal("real", result) 106 | 107 | case "Varchar": 108 | assert.Equal("text", result) 109 | case "LongText": 110 | assert.Equal("text", result) 111 | 112 | case "Time": 113 | assert.Equal("text", result) 114 | 115 | case "Slice": 116 | assert.Equal("text", result) 117 | 118 | case "DerivedInt": 119 | assert.Equal("integer", result) 120 | case "DerivedInt16": 121 | assert.Equal("integer", result) 122 | case "DerivedBool": 123 | assert.Equal("integer", result) 124 | case "DerivedFloat": 125 | assert.Equal("real", result) 126 | case "DerivedTime": 127 | assert.Equal("text", result) 128 | } 129 | }*/ 130 | } 131 | 132 | func TestSqlite3Transaction(t *testing.T) { 133 | registerSqlite3Test() 134 | doTestTransaction(NewAssert(t)) 135 | } 136 | 137 | func TestSqlite3SaveAndDelete(t *testing.T) { 138 | mg, q := setupSqlite3Db() 139 | doTestSaveAndDelete(NewAssert(t), mg, q) 140 | } 141 | 142 | func TestSqlite3SaveAgain(t *testing.T) { 143 | mg, q := setupSqlite3Db() 144 | doTestSaveAgain(NewAssert(t), mg, q) 145 | } 146 | 147 | func TestSqlite3ForeignKey(t *testing.T) { 148 | registerSqlite3Test() 149 | doTestForeignKey(NewAssert(t)) 150 | } 151 | 152 | func TestSqlite3Find(t *testing.T) { 153 | registerSqlite3Test() 154 | doTestFind(NewAssert(t)) 155 | } 156 | 157 | func TestSqlite3CreateTable(t *testing.T) { 158 | mg, _ := setupSqlite3Db() 159 | doTestCreateTable(NewAssert(t), mg) 160 | } 161 | 162 | func TestSqlite3Update(t *testing.T) { 163 | mg, q := setupSqlite3Db() 164 | doTestUpdate(NewAssert(t), mg, q) 165 | } 166 | 167 | func TestSqlite3Validation(t *testing.T) { 168 | mg, q := setupSqlite3Db() 169 | doTestValidation(NewAssert(t), mg, q) 170 | } 171 | 172 | func TestSqlite3BoolType(t *testing.T) { 173 | mg, q := setupSqlite3Db() 174 | doTestBoolType(NewAssert(t), mg, q) 175 | } 176 | 177 | func TestSqlite3StringPk(t *testing.T) { 178 | mg, q := setupSqlite3Db() 179 | doTestStringPk(NewAssert(t), mg, q) 180 | } 181 | 182 | func TestSqlite3Count(t *testing.T) { 183 | registerSqlite3Test() 184 | doTestCount(NewAssert(t)) 185 | } 186 | 187 | func TestSqlite3QueryMap(t *testing.T) { 188 | mg, q := setupSqlite3Db() 189 | doTestQueryMap(NewAssert(t), mg, q) 190 | } 191 | 192 | func TestSqlite3BulkInsert(t *testing.T) { 193 | registerSqlite3Test() 194 | doTestBulkInsert(NewAssert(t)) 195 | } 196 | 197 | func TestSqlite3QueryStruct(t *testing.T) { 198 | registerSqlite3Test() 199 | doTestQueryStruct(NewAssert(t)) 200 | } 201 | 202 | func TestSqlite3CustomNameConvertion(t *testing.T) { 203 | registerSqlite3Test() 204 | ColumnNameToFieldName = noConvert 205 | FieldNameToColumnName = noConvert 206 | TableNameToStructName = noConvert 207 | StructNameToTableName = noConvert 208 | doTestForeignKey(NewAssert(t)) 209 | ColumnNameToFieldName = snakeToUpperCamel 210 | FieldNameToColumnName = toSnake 211 | TableNameToStructName = snakeToUpperCamel 212 | StructNameToTableName = toSnake 213 | } 214 | 215 | func TestSqlite3ConnectionLimit(t *testing.T) { 216 | registerSqlite3Test() 217 | doTestConnectionLimit(NewAssert(t)) 218 | } 219 | 220 | func TestSqlite3Iterate(t *testing.T) { 221 | registerSqlite3Test() 222 | doTestIterate(NewAssert(t)) 223 | } 224 | 225 | func TestSqlite3AddColumnSQL(t *testing.T) { 226 | doTestAddColumSQL(NewAssert(t), sqlite3Syntax) 227 | } 228 | 229 | func TestSqlite3CreateTableSQL(t *testing.T) { 230 | doTestCreateTableSQL(NewAssert(t), sqlite3Syntax) 231 | } 232 | 233 | func TestSqlite3CreateIndexSQL(t *testing.T) { 234 | doTestCreateIndexSQL(NewAssert(t), sqlite3Syntax) 235 | } 236 | 237 | func TestSqlite3InsertSQL(t *testing.T) { 238 | doTestInsertSQL(NewAssert(t), sqlite3Syntax) 239 | } 240 | 241 | func TestSqlite3UpdateSQL(t *testing.T) { 242 | doTestUpdateSQL(NewAssert(t), sqlite3Syntax) 243 | } 244 | 245 | func TestSqlite3DeleteSQL(t *testing.T) { 246 | doTestDeleteSQL(NewAssert(t), sqlite3Syntax) 247 | } 248 | 249 | func TestSqlite3SelectionSQL(t *testing.T) { 250 | doTestSelectionSQL(NewAssert(t), sqlite3Syntax) 251 | } 252 | 253 | func TestSqlite3QuerySQL(t *testing.T) { 254 | doTestQuerySQL(NewAssert(t), sqlite3Syntax) 255 | } 256 | 257 | func TestSqlite3DropTableSQL(t *testing.T) { 258 | doTestDropTableSQL(NewAssert(t), sqlite3Syntax) 259 | } 260 | 261 | func TestSqlite3SaveNullable(t *testing.T) { 262 | mg, q := setupSqlite3Db() 263 | doTestSaveNullable(NewAssert(t), mg, q) 264 | } 265 | 266 | func BenchmarkSqlite3Find(b *testing.B) { 267 | registerSqlite3Test() 268 | doBenchmarkFind(b, b.N) 269 | } 270 | 271 | func BenchmarkSqlite3DbQuery(b *testing.B) { 272 | registerSqlite3Test() 273 | doBenchmarkDbQuery(b, b.N) 274 | } 275 | 276 | func BenchmarkSqlite3StmtQuery(b *testing.B) { 277 | registerSqlite3Test() 278 | doBenchmarkStmtQuery(b, b.N) 279 | } 280 | 281 | func BenchmarkSqlite3Transaction(b *testing.B) { 282 | registerSqlite3Test() 283 | doBenchmarkTransaction(b, b.N) 284 | } 285 | -------------------------------------------------------------------------------- /README_ZH.md: -------------------------------------------------------------------------------- 1 | Qbs 2 | ===== 3 | 4 | Qbs是一个Go语言的ORM 5 | 6 | ##特性 7 | 8 | * 支持通过struct定义表结构,自动建表。 9 | * 如果表已经存在,而struct定义了新的字段,Qbs会自动向数据库表添加相应的字段。 10 | * 在查询时,struct里的字段会映射到"SELECT"语句里。 11 | * 通过在struct里添加一个对应着父表的struct指针字段来实现关联查询。 12 | * 增删改查都通过struct来实现。 13 | * 查询后,需要的数据通过struct来取得。 14 | * 通过Condition来编写查询条件,可以轻松地组织不同优先级的多个AND、OR子条件。 15 | * 如果struct里包含Id字段,而且值大于零,这个字段的值会被视为查询条件,添加到Where语句里。 16 | * 可以通过字段名或tag定义created和updated字段,当插入/更新时,会自动更新为当前时间。 17 | * struct可以通过实现Validator interface,在插入或更新之前对数据进行验证。 18 | * 目前支持MySQL, PosgreSQL, SQLite3,即将支持Oracle。 19 | * 支持连接池。 20 | 21 | ##安装 22 | 23 | go get github.com/coocood/qbs 24 | 25 | ##API文档 26 | 27 | [GoDoc](http://godoc.org/github.com/coocood/qbs) 28 | 29 | ##注意 30 | 31 | * 新的版本可能会不兼容旧的API,使旧程序不能正常工作。 32 | * 一次go get下载后,请保留当时的版本,如果需要其它机器上编辑,请复制当时的版本,不要在新的机器上通过go get来下载最新版本。 33 | * 或者Fork一下,版本的更新自己来掌握。 34 | * 每一次进行有可能破坏兼容性的更新时,会把之前的版本保存为一个新的branch, 名字是更新的日期。 35 | 36 | ##使用手册 37 | 38 | 39 | ### 首先要注册数据库: 40 | - 参数比打开数据库要多两个,分别是数据库名:"qbs_test"和Dialect:`qbs.NewMysql()`。 41 | - 一般只需要在应用启动是执行一次。 42 | 43 | func RegisterDb(){ 44 | qbs.Register("mysql","qbs_test@/qbs_test?charset=utf8&parseTime=true&loc=Local", "qbs_test", qbs.NewMysql()) 45 | } 46 | 47 | 48 | ### 定义一个`User`类型: 49 | - 如果字段字为`Id`而且类型为`int64`的话,会被Qbs视为主键。如果想用`Id`以外的名字做为主键名,可以在后加上`qbs:"pk"`来定义主键。 50 | - `Name`后面的标签`qbs:"size:32,index"`用来定义建表时的字段属性。属性在双引号中定义,多个不同的属性用逗号区分,中间没有空格。 51 | - 这里用到两个属性,一个是`size`,值是32,对应的SQL语句是`varchar(32)`。 52 | - 另一个属性是`index`,建立这个字段的索引。也可以用`unique`来定义唯一约束索引。 53 | - string类型的size属性很重要,如果加上size,而且size在数据库支持的范围内,会生成定长的varchar类型,不加size的话,对应的数据库类型是不定长的,有的数据库(MySQL)无法建立索引。 54 | 55 | 56 | type User struct { 57 | Id int64 58 | Name string `qbs:"size:32,index"` 59 | } 60 | 61 | - 如果需要联合索引,需要实现Indexes方法。 62 | 63 | 64 | func (*User) Indexes(indexes *qbs.Indexes){ 65 | //indexes.Add("column_a", "column_b") or indexes.AddUnique("column_a", "column_b") 66 | } 67 | 68 | 69 | ### 新建表: 70 | - `qbs.NewMysql`函数创建数据库的Dialect(方言),因为不同数据库的SQL语句和数据类型有差异,所以需要不同的Dialect来适配。每个Qbs支持的数据库都有相应的Dialect函数。 71 | - `qbs.NewMigration`函数用来创建Migration实例,用来进行建表操作。和数据库的CRUD操作的Qbs实例是分开的。 72 | - 建表时,即使表已存在,如果发现有新增的字段或索引,会自动执行添加字段和索引的操作。 73 | - 建表方法建议在程序启动时调用,而且完全可以用在产品数据库上。因为所有的迁移操作都是增量的,非破坏性的,所以不会有数据丢失的风险。 74 | - `CreateTableIfNotExists`方法的参数必须是struct指针,不然会panic。 75 | 76 | 77 | func CreateUserTable() error{ 78 | migration, err := qbs.GetMigration() 79 | if err != nil { 80 | return err 81 | } 82 | defer migration.Close() 83 | return migration.CreateTableIfNotExists(new(User)) 84 | } 85 | 86 | 87 | ### 获取和使用`*qbs.Qbs`实例: 88 | - 假设需要在一个http请求中获取和使用Qbs. 89 | - 取得Qbs实例后,应该马上执行`defer q.Close()`来回收数据库连接。 90 | - qbs使用连接池,默认大小为100,可以通过在应用启动时,调用`qbs.ChangePoolSize()`来修改。 91 | 92 | func GetUser(w http.ResponseWriter, r *http.Request){ 93 | q, err := qbs.GetQbs() 94 | if err != nil { 95 | fmt.Println(err) 96 | w.WriteHeader(500) 97 | return 98 | } 99 | defer q.Close() 100 | u, err := FindUserById(q, 6) 101 | data, _ := json.Marshal(u) 102 | w.Write(data) 103 | } 104 | 105 | ### 插入数据: 106 | - 如果处理一个请求需要多次进行数据库操作,最好在函数间传递*Qbs参数,这样只需要执行一次获取关闭操作就可以了。 107 | - 插入数据时使用`Save`方法,如果`user`的主键Id没有赋值,`Save`会执行INSERT语句。 108 | - 如果`user`的`Id`是一个正整数,`Save`会首先执行一次SELECT COUNT操作,如果发现count为0,会执行INSERT语句,否则会执行UPDATE语句。 109 | - `Save`的参数必须是struct指针,不然会panic。 110 | 111 | 112 | func CreateUser(q *qbs.Qbs) (*User,error){ 113 | user := new(User) 114 | user.Name = "Green" 115 | _, err := q.Save(user) 116 | return user,err 117 | } 118 | 119 | ### 查询数据: 120 | - 如果需要根据Id主键查询,只要给user的Id赋值就可以了。 121 | 122 | 123 | func FindUserById(q *qbs.Qbs, id int64) (*User, error) { 124 | user := new(User) 125 | user.Id = id 126 | err := q.Find(user) 127 | return user, err 128 | } 129 | 130 | 131 | - 查询多行需要调用`FindAll`,参数必须是slice的指针,slice的元素必须是struct的指针。 132 | 133 | 134 | func FindUsers(q *qbs.Qbs) ([]*User, error) { 135 | var users []*User 136 | err := q.Limit(10).Offset(10).FindAll(&users) 137 | return users, err 138 | } 139 | 140 | 141 | - 其它的查询条件,需要调用`Where`方法。这里的`WhereEqual("name", name)`相当于`Where("name = ?", name)`,只是一个简写形式。 142 | - `Where`/`WhereEqual`只有最后一次调用有效,之前调用的条件会被后面的覆盖掉,适用于简单的查询条件,。 143 | - 注意,这里第一个参数字段名是`"name"`,而不是struct里的`"Name"`。所有代码里的`AbCd`形式的字段名,或类型名,在储存到数据库时会被转化为`ab_cd`的形式。 144 | 这样做的目的是为了符合go的命名规范,方便json序列化,同时避免大小写造成的数据库迁移错误。 145 | 146 | 147 | func FindUserByName(q *qbs.Qbs, n string) (*User, error) { 148 | user := new(User) 149 | err := q.WhereEqual("name", n).Find(user) 150 | return user, err 151 | } 152 | 153 | 154 | - 如果需要定义复杂的查询条件,可以调用`Condition`方法。参数类型为`*Condition`,通过`NewCondition`或`NewEqualCondition`、`NewInCondition`函数来新建。 155 | - `*Condition`类型支持`And`、`Or`等方法,可以连续调用。 156 | - `Condition`方法同样也只能调用一次,而且不可以和`Where`同时使用。 157 | 158 | 159 | func FindUserByCondition(q *qbs.Qbs) (*User, error) { 160 | user := new(User) 161 | condition1 := qbs.NewCondition("id > ?", 100).Or("id < ?", 50).OrEqual("id", 75) 162 | condition2 := qbs.NewCondition("name != ?", "Red").And("name != ?", "Black") 163 | condition1.AndCondition(condition2) 164 | err := q.Condition(condition1).Find(user) 165 | return user, err 166 | } 167 | 168 | 169 | ### 更新一行: 170 | - 更新一行数据需要先`Find`,再`Save`。 171 | 172 | 173 | func UpdateOneUser(q *qbs.Qbs, id int64, name string) (affected int64, error){ 174 | user, err := FindUserById(q, id) 175 | if err != nil { 176 | return 0, err 177 | } 178 | user.Name = name 179 | return q.Save(user) 180 | } 181 | 182 | 183 | ### 更新多行: 184 | - 多行的更新需要调用`Update`,需要注意的是,如果使用包含所有字段的struct,会把所有的字段都更新,这不会是想要的结果。 185 | 解决办法是在函数里定义临时的struct,只包含需要更新的字段。如果在函数里需要用到同名的struct,可以把冲突的部分放在block里`{...}`。 186 | 187 | 188 | func UpdateMultipleUsers(q *qbs.Qbs)(affected int64, error) { 189 | type User struct { 190 | Name string 191 | } 192 | user := new(User) 193 | user.Name = "Blue" 194 | return q.WhereEqual("name", "Green").Update(user) 195 | } 196 | 197 | ### 删除: 198 | - 删除时条件不可以为空,要么在Id字段定义,要么在Where或Condition里定义。 199 | 200 | 201 | func DeleteUser(q *qbs.Qbs, id int64)(affected int64, err error) { 202 | user := new(User) 203 | user.Id = id 204 | return q.Delete(user) 205 | } 206 | 207 | ### 定义需要关联查询的表: 208 | - 这里`Post`里包含了一个名为`AuthorId`,类型为`int64`的字段,而且同时包含一个名为`Author`,类型为`*User`的字段。 209 | - 使用类似 `{xxx}Id int64`, `{xxx} *{yyy}` 这样的格式,就可以定义关联查询。 210 | - 这里`Author`这个字段因为是指针类型,所以在`Post`建表时不会被添加为column。 211 | - 建表时,因为检测到关联字段,所以会自动为`author_id`建立索引。关联字段不需要在tag里定义索引。 212 | - 关联字段名可以不符合以上格式,只要明确地在`AuthorId`的tag里加上`qbs:"join:Author"`,同样可以定义关联查询。 213 | - 定义外键约束需要明确地在`AuthorId`对应的tag里添加`qbs:"fk:Author"`。 214 | - 定义外键的同时,也就相当于定义了关联查询,同样会自动建立索引,区别仅仅是建表时添加了外键约束的语句。 215 | - `Created time.Time`字段会在插入时写入当前时间,`Updated time.Time`字段会在更新时自动更新为当前时间。 216 | - 如果想给自动赋值的时间字段用其它字段名,不想用"Created","Updated",可以在tag里添加`qbs:"created"`,`qbs:"updated"`。 217 | 218 | 219 | type Post struct { 220 | Id int64 221 | AuthorId int64 222 | Author *User 223 | Content string 224 | Created time.Time 225 | Updated time.Time 226 | } 227 | 228 | 229 | ### 查询时忽略某些字段: 230 | - 有时候,我们查询时并不需要某些字段,特别是关联查询的字段(比如`Author`字段),或数据很大的字段(比如`Content`字段) 231 | ,如果忽略掉,会提高查询效率。 232 | 233 | 234 | func FindPostsOmitContentAndCreated(q *qbs.Qbs) ([]*Post, error) { 235 | var posts []*Post 236 | err := q.OmitFields("Content","Created").Find(&posts) 237 | return posts, err 238 | } 239 | 240 | 241 | ### 查询时忽略关联字段: 242 | - 如果struct里定义了关联查询,每次Find都会自动JOIN,不需要特别指定,但有时候,我们在某一次查询时并不需要关联查询, 243 | 这时忽略掉关联查询会提高查询效率。当然我们可以用`OmitFields`实现同样的效果,但是那样需要在参数里手写字段名,不够简洁。 244 | - 使用`OmitJoin`可以忽略所有关联查询,只返回单一表的数据,和`OmitFields`可以同时使用。 245 | 246 | 247 | func FindPostsOmitJoin(q *qbs.Qbs) ([]*Post, error) { 248 | var posts []*Post 249 | err := q.OmitJoin().OmitFields("Content").Find(&posts) 250 | return posts, err 251 | } 252 | 253 | 。。。未完代续 -------------------------------------------------------------------------------- /base.go: -------------------------------------------------------------------------------- 1 | package qbs 2 | 3 | import ( 4 | "bytes" 5 | "database/sql" 6 | "fmt" 7 | "reflect" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | type base struct { 13 | dialect Dialect 14 | } 15 | 16 | func (d base) substituteMarkers(query string) string { 17 | return query 18 | } 19 | 20 | func (d base) quote(s string) string { 21 | segs := strings.Split(s, ".") 22 | buf := new(bytes.Buffer) 23 | buf.WriteByte('`') 24 | buf.WriteString(segs[0]) 25 | for i := 1; i < len(segs); i++ { 26 | buf.WriteString("`.`") 27 | buf.WriteString(segs[i]) 28 | } 29 | buf.WriteByte('`') 30 | return buf.String() 31 | } 32 | 33 | func (d base) parseBool(value reflect.Value) bool { 34 | return value.Bool() 35 | } 36 | 37 | func (d base) setPtrValue(driverValue, fieldValue reflect.Value) { 38 | t := fieldValue.Type().Elem() 39 | v := reflect.New(t) 40 | fieldValue.Set(v) 41 | switch t.Kind() { 42 | case reflect.String: 43 | v.Elem().SetString(string(driverValue.Interface().([]uint8))) 44 | case reflect.Int64: 45 | v.Elem().SetInt(driverValue.Interface().(int64)) 46 | case reflect.Float64: 47 | v.Elem().SetFloat(driverValue.Interface().(float64)) 48 | case reflect.Bool: 49 | v.Elem().SetBool(driverValue.Interface().(bool)) 50 | } 51 | } 52 | func (d base) setModelValue(driverValue, fieldValue reflect.Value) error { 53 | switch fieldValue.Type().Kind() { 54 | case reflect.Bool: 55 | fieldValue.SetBool(d.dialect.parseBool(driverValue.Elem())) 56 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 57 | fieldValue.SetInt(driverValue.Elem().Int()) 58 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 59 | // reading uint from int value causes panic 60 | switch driverValue.Elem().Kind() { 61 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 62 | fieldValue.SetUint(uint64(driverValue.Elem().Int())) 63 | default: 64 | fieldValue.SetUint(driverValue.Elem().Uint()) 65 | } 66 | case reflect.Float32, reflect.Float64: 67 | fieldValue.SetFloat(driverValue.Elem().Float()) 68 | case reflect.String: 69 | fieldValue.SetString(string(driverValue.Elem().Bytes())) 70 | case reflect.Slice: 71 | if reflect.TypeOf(driverValue.Interface()).Elem().Kind() == reflect.Uint8 { 72 | fieldValue.SetBytes(driverValue.Elem().Bytes()) 73 | } 74 | case reflect.Ptr: 75 | d.setPtrValue(driverValue, fieldValue) 76 | case reflect.Struct: 77 | switch fieldValue.Interface().(type) { 78 | case time.Time: 79 | fieldValue.Set(driverValue.Elem()) 80 | default: 81 | if scanner, ok := fieldValue.Addr().Interface().(sql.Scanner); ok { 82 | return scanner.Scan(driverValue.Interface()) 83 | } 84 | } 85 | 86 | } 87 | return nil 88 | } 89 | 90 | func (d base) querySql(criteria *criteria) (string, []interface{}) { 91 | query := new(bytes.Buffer) 92 | args := make([]interface{}, 0, 20) 93 | table := d.dialect.quote(criteria.model.table) 94 | columns := []string{} 95 | tables := []string{table} 96 | hasJoin := len(criteria.model.refs) > 0 97 | for _, v := range criteria.model.fields { 98 | colName := d.dialect.quote(v.name) 99 | if hasJoin { 100 | colName = d.dialect.quote(criteria.model.table) + "." + colName 101 | } 102 | columns = append(columns, colName) 103 | } 104 | for k, v := range criteria.model.refs { 105 | tableAlias := StructNameToTableName(k) 106 | quotedTableAlias := d.dialect.quote(tableAlias) 107 | quotedParentTable := d.dialect.quote(v.model.table) 108 | leftKey := table + "." + d.dialect.quote(v.refKey) 109 | parentPrimary := quotedTableAlias + "." + d.dialect.quote(v.model.pk.name) 110 | joinClause := fmt.Sprintf("LEFT JOIN %v AS %v ON %v = %v", quotedParentTable, quotedTableAlias, leftKey, parentPrimary) 111 | tables = append(tables, joinClause) 112 | for _, f := range v.model.fields { 113 | alias := tableAlias + "___" + f.name 114 | columns = append(columns, d.dialect.quote(tableAlias+"."+f.name)+" AS "+alias) 115 | } 116 | } 117 | query.WriteString("SELECT ") 118 | query.WriteString(strings.Join(columns, ", ")) 119 | query.WriteString(" FROM ") 120 | query.WriteString(strings.Join(tables, " ")) 121 | 122 | if criteria.condition != nil { 123 | cexpr, cargs := criteria.condition.Merge() 124 | query.WriteString(" WHERE ") 125 | query.WriteString(cexpr) 126 | args = append(args, cargs...) 127 | } 128 | orderByLen := len(criteria.orderBys) 129 | if orderByLen > 0 { 130 | query.WriteString(" ORDER BY ") 131 | for i, order := range criteria.orderBys { 132 | query.WriteString(order.path) 133 | if order.desc { 134 | query.WriteString(" DESC") 135 | } 136 | if i < orderByLen-1 { 137 | query.WriteString(", ") 138 | } 139 | } 140 | } 141 | 142 | if x := criteria.limit; x > 0 { 143 | query.WriteString(" LIMIT ?") 144 | args = append(args, criteria.limit) 145 | } 146 | if x := criteria.offset; x > 0 { 147 | query.WriteString(" OFFSET ?") 148 | args = append(args, criteria.offset) 149 | } 150 | return d.dialect.substituteMarkers(query.String()), args 151 | } 152 | 153 | func (d base) insert(q *Qbs) (int64, error) { 154 | sql, args := d.dialect.insertSql(q.criteria) 155 | result, err := q.Exec(sql, args...) 156 | if err != nil { 157 | return -1, err 158 | } 159 | id, err := result.LastInsertId() 160 | if err != nil { 161 | return -1, err 162 | } 163 | return id, nil 164 | } 165 | 166 | func (d base) insertSql(criteria *criteria) (string, []interface{}) { 167 | columns, values := criteria.model.columnsAndValues(false) 168 | quotedColumns := make([]string, 0, len(columns)) 169 | markers := make([]string, 0, len(columns)) 170 | for _, c := range columns { 171 | quotedColumns = append(quotedColumns, d.dialect.quote(c)) 172 | markers = append(markers, "?") 173 | } 174 | sql := fmt.Sprintf( 175 | "INSERT INTO %v (%v) VALUES (%v)", 176 | d.dialect.quote(criteria.model.table), 177 | strings.Join(quotedColumns, ", "), 178 | strings.Join(markers, ", "), 179 | ) 180 | return sql, values 181 | } 182 | 183 | func (d base) update(q *Qbs) (int64, error) { 184 | sql, args := d.dialect.updateSql(q.criteria) 185 | result, err := q.Exec(sql, args...) 186 | if err != nil { 187 | return 0, err 188 | } 189 | affected, err := result.RowsAffected() 190 | return affected, err 191 | } 192 | 193 | func (d base) updateSql(criteria *criteria) (string, []interface{}) { 194 | columns, values := criteria.model.columnsAndValues(true) 195 | pairs := make([]string, 0, len(columns)) 196 | for _, column := range columns { 197 | pairs = append(pairs, fmt.Sprintf("%v = ?", d.dialect.quote(column))) 198 | } 199 | conditionSql, args := criteria.condition.Merge() 200 | sql := fmt.Sprintf( 201 | "UPDATE %v SET %v WHERE %v", 202 | d.dialect.quote(criteria.model.table), 203 | strings.Join(pairs, ", "), 204 | conditionSql, 205 | ) 206 | values = append(values, args...) 207 | return sql, values 208 | } 209 | 210 | func (d base) delete(q *Qbs) (int64, error) { 211 | sql, args := d.dialect.deleteSql(q.criteria) 212 | result, err := q.Exec(sql, args...) 213 | if err != nil { 214 | return 0, err 215 | } 216 | return result.RowsAffected() 217 | } 218 | 219 | func (d base) deleteSql(criteria *criteria) (string, []interface{}) { 220 | conditionSql, args := criteria.condition.Merge() 221 | sql := "DELETE FROM " + d.dialect.quote(criteria.model.table) + " WHERE " + conditionSql 222 | return sql, args 223 | } 224 | 225 | func (d base) createTableSql(model *model, ifNotExists bool) string { 226 | a := []string{"CREATE TABLE "} 227 | if ifNotExists { 228 | a = append(a, "IF NOT EXISTS ") 229 | } 230 | a = append(a, d.dialect.quote(model.table), " ( ") 231 | for i, field := range model.fields { 232 | b := []string{ 233 | d.dialect.quote(field.name), 234 | } 235 | if field.pk { 236 | _, ok := field.value.(string) 237 | b = append(b, d.dialect.primaryKeySql(ok, field.size)) 238 | } else { 239 | b = append(b, d.dialect.sqlType(*field)) 240 | if field.notnull { 241 | b = append(b, "NOT NULL") 242 | } 243 | if x := field.dfault; x != "" { 244 | b = append(b, "DEFAULT "+x) 245 | } 246 | } 247 | a = append(a, strings.Join(b, " ")) 248 | if i < len(model.fields)-1 { 249 | a = append(a, ", ") 250 | } 251 | } 252 | for _, v := range model.refs { 253 | if v.foreignKey { 254 | a = append(a, ", FOREIGN KEY (", d.dialect.quote(v.refKey), ") REFERENCES ") 255 | a = append(a, d.dialect.quote(v.model.table), " (", d.dialect.quote(v.model.pk.name), ") ON DELETE CASCADE") 256 | } 257 | } 258 | a = append(a, " )") 259 | return strings.Join(a, "") 260 | } 261 | 262 | func (d base) dropTableSql(table string) string { 263 | a := []string{"DROP TABLE IF EXISTS"} 264 | a = append(a, d.dialect.quote(table)) 265 | return strings.Join(a, " ") 266 | } 267 | 268 | func (d base) addColumnSql(table string, column modelField) string { 269 | return fmt.Sprintf( 270 | "ALTER TABLE %v ADD COLUMN %v %v", 271 | d.dialect.quote(table), 272 | d.dialect.quote(column.name), 273 | d.dialect.sqlType(column), 274 | ) 275 | } 276 | 277 | func (d base) createIndexSql(name, table string, unique bool, columns ...string) string { 278 | a := []string{"CREATE"} 279 | if unique { 280 | a = append(a, "UNIQUE") 281 | } 282 | quotedColumns := make([]string, 0, len(columns)) 283 | for _, c := range columns { 284 | quotedColumns = append(quotedColumns, d.dialect.quote(c)) 285 | } 286 | a = append(a, fmt.Sprintf( 287 | "INDEX %v ON %v (%v)", 288 | d.dialect.quote(name), 289 | d.dialect.quote(table), 290 | strings.Join(quotedColumns, ", "), 291 | )) 292 | return strings.Join(a, " ") 293 | } 294 | 295 | func (d base) columnsInTable(mg *Migration, table interface{}) map[string]bool { 296 | tn := tableName(table) 297 | columns := make(map[string]bool) 298 | query := "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?" 299 | query = mg.dialect.substituteMarkers(query) 300 | rows, err := mg.db.Query(query, mg.dbName, tn) 301 | defer rows.Close() 302 | if err != nil { 303 | panic(err) 304 | } 305 | for rows.Next() { 306 | column := "" 307 | err := rows.Scan(&column) 308 | if err == nil { 309 | columns[column] = true 310 | } 311 | } 312 | return columns 313 | } 314 | 315 | func (d base) catchMigrationError(err error) bool { 316 | return false 317 | } 318 | -------------------------------------------------------------------------------- /model.go: -------------------------------------------------------------------------------- 1 | package qbs 2 | 3 | import ( 4 | "bytes" 5 | "database/sql" 6 | "reflect" 7 | "strconv" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | type TableNamer interface { 13 | TableName() string 14 | } 15 | 16 | const QBS_COLTYPE_INT = "int" 17 | const QBS_COLTYPE_BOOL = "boolean" 18 | const QBS_COLTYPE_BIGINT = "bigint" 19 | const QBS_COLTYPE_DOUBLE = "double" 20 | const QBS_COLTYPE_TIME = "timestamp" 21 | const QBS_COLTYPE_TEXT = "text" 22 | 23 | //convert struct field name to column name. 24 | var FieldNameToColumnName func(string) string = toSnake 25 | 26 | //convert struct name to table name. 27 | var StructNameToTableName func(string) string = toSnake 28 | 29 | //onvert column name to struct field name. 30 | var ColumnNameToFieldName func(string) string = snakeToUpperCamel 31 | 32 | //convert table name to struct name. 33 | var TableNameToStructName func(string) string = snakeToUpperCamel 34 | 35 | // Index represents a table index and is returned via the Indexed interface. 36 | type index struct { 37 | name string 38 | columns []string 39 | unique bool 40 | } 41 | 42 | // Indexes represents an array of indexes. 43 | type Indexes []*index 44 | 45 | type Indexed interface { 46 | Indexes(indexes *Indexes) 47 | } 48 | 49 | // Add adds an index 50 | func (ix *Indexes) Add(columns ...string) { 51 | name := strings.Join(columns, "_") 52 | *ix = append(*ix, &index{name: name, columns: columns, unique: false}) 53 | } 54 | 55 | // AddUnique adds an unique index 56 | func (ix *Indexes) AddUnique(columns ...string) { 57 | name := strings.Join(columns, "_") 58 | *ix = append(*ix, &index{name: name, columns: columns, unique: true}) 59 | } 60 | 61 | // ModelField represents a schema field of a parsed model. 62 | type modelField struct { 63 | name string // Column name 64 | camelName string 65 | value interface{} // Value 66 | pk bool 67 | notnull bool 68 | index bool 69 | unique bool 70 | updated bool 71 | created bool 72 | size int 73 | dfault string 74 | fk string 75 | join string 76 | colType string 77 | nullable reflect.Kind 78 | } 79 | 80 | // Model represents a parsed schema interface{}. 81 | type model struct { 82 | pk *modelField 83 | table string 84 | fields []*modelField 85 | refs map[string]*reference 86 | indexes Indexes 87 | } 88 | 89 | type reference struct { 90 | refKey string 91 | model *model 92 | foreignKey bool 93 | } 94 | 95 | func (model *model) columnsAndValues(forUpdate bool) ([]string, []interface{}) { 96 | columns := make([]string, 0, len(model.fields)) 97 | values := make([]interface{}, 0, len(columns)) 98 | for _, column := range model.fields { 99 | var include bool 100 | if forUpdate { 101 | include = column.value != nil && !column.pk 102 | } else { 103 | include = true 104 | if column.value == nil && column.nullable == reflect.Invalid { 105 | include = false 106 | } else if column.pk { 107 | if intValue, ok := column.value.(int64); ok { 108 | include = intValue != 0 109 | } else if strValue, ok := column.value.(string); ok { 110 | include = strValue != "" 111 | } 112 | } 113 | } 114 | if include { 115 | columns = append(columns, column.name) 116 | values = append(values, column.value) 117 | } 118 | } 119 | return columns, values 120 | } 121 | 122 | func (model *model) timeField(name string) *modelField { 123 | for _, v := range model.fields { 124 | if _, ok := v.value.(time.Time); ok { 125 | if name == "created" { 126 | if v.created { 127 | return v 128 | } 129 | } else if name == "updated" { 130 | if v.updated { 131 | return v 132 | } 133 | } 134 | if v.name == name { 135 | return v 136 | } 137 | } 138 | } 139 | return nil 140 | } 141 | 142 | func (model *model) pkZero() bool { 143 | if model.pk == nil { 144 | return true 145 | } 146 | switch model.pk.value.(type) { 147 | case string: 148 | return model.pk.value.(string) == "" 149 | case int8: 150 | return model.pk.value.(int8) == 0 151 | case int16: 152 | return model.pk.value.(int16) == 0 153 | case int32: 154 | return model.pk.value.(int32) == 0 155 | case int64: 156 | return model.pk.value.(int64) == 0 157 | case uint8: 158 | return model.pk.value.(uint8) == 0 159 | case uint16: 160 | return model.pk.value.(uint16) == 0 161 | case uint32: 162 | return model.pk.value.(uint32) == 0 163 | case uint64: 164 | return model.pk.value.(uint64) == 0 165 | } 166 | return true 167 | } 168 | 169 | func structPtrToModel(f interface{}, root bool, omitFields []string) *model { 170 | model := &model{ 171 | pk: nil, 172 | table: tableName(f), 173 | fields: []*modelField{}, 174 | indexes: Indexes{}, 175 | } 176 | structType := reflect.TypeOf(f).Elem() 177 | structValue := reflect.ValueOf(f).Elem() 178 | if structType.Kind() == reflect.Ptr { 179 | if structType.Elem().Kind() == reflect.Struct { 180 | panic("did you pass a pointer to a pointer to a struct?") 181 | } 182 | } 183 | for i := 0; i < structType.NumField(); i++ { 184 | structField := structType.Field(i) 185 | omit := false 186 | for _, v := range omitFields { 187 | if v == structField.Name { 188 | omit = true 189 | } 190 | } 191 | if omit { 192 | continue 193 | } 194 | fieldValue := structValue.FieldByName(structField.Name) 195 | if !fieldValue.CanInterface() { 196 | continue 197 | } 198 | sqlTag := structField.Tag.Get("qbs") 199 | if sqlTag == "-" { 200 | continue 201 | } 202 | fieldIsNullable := false 203 | kind := structField.Type.Kind() 204 | switch kind { 205 | case reflect.Ptr: 206 | switch structField.Type.Elem().Kind() { 207 | case reflect.Bool, reflect.String, reflect.Int64, reflect.Float64: 208 | kind = structField.Type.Elem().Kind() 209 | fieldIsNullable = true 210 | default: 211 | continue 212 | } 213 | case reflect.Map: 214 | continue 215 | case reflect.Slice: 216 | elemKind := structField.Type.Elem().Kind() 217 | if elemKind != reflect.Uint8 { 218 | continue 219 | } 220 | } 221 | 222 | fd := new(modelField) 223 | parseTags(fd, sqlTag) 224 | fd.camelName = structField.Name 225 | fd.name = FieldNameToColumnName(structField.Name) 226 | if fieldIsNullable { 227 | fd.nullable = kind 228 | if fieldValue.IsNil() { 229 | fd.value = nil 230 | } else { 231 | fd.value = fieldValue.Elem().Interface() 232 | } 233 | } else { 234 | //not nullable case 235 | fd.value = fieldValue.Interface() 236 | } 237 | if _, ok := fd.value.(int64); ok && fd.camelName == "Id" { 238 | fd.pk = true 239 | } 240 | if fd.pk { 241 | model.pk = fd 242 | } 243 | 244 | model.fields = append(model.fields, fd) 245 | // fill in references map only in root model. 246 | if root { 247 | var fk, explicitJoin, implicitJoin bool 248 | var refName string 249 | if fd.fk != "" { 250 | refName = fd.fk 251 | fk = true 252 | } else if fd.join != "" { 253 | refName = fd.join 254 | explicitJoin = true 255 | } 256 | 257 | if len(fd.camelName) > 3 && strings.HasSuffix(fd.camelName, "Id") { 258 | fdValue := reflect.ValueOf(fd.value) 259 | if _, ok := fd.value.(sql.NullInt64); ok || fdValue.Kind() == reflect.Int64 { 260 | i := strings.LastIndex(fd.camelName, "Id") 261 | refName = fd.camelName[:i] 262 | implicitJoin = true 263 | } 264 | } 265 | 266 | if fk || explicitJoin || implicitJoin { 267 | omit := false 268 | for _, v := range omitFields { 269 | if v == refName { 270 | omit = true 271 | } 272 | } 273 | if field, ok := structType.FieldByName(refName); ok && !omit { 274 | fieldValue := structValue.FieldByName(refName) 275 | if fieldValue.Kind() == reflect.Ptr { 276 | model.indexes.Add(fd.name) 277 | if fieldValue.IsNil() { 278 | fieldValue.Set(reflect.New(field.Type.Elem())) 279 | } 280 | refModel := structPtrToModel(fieldValue.Interface(), false, nil) 281 | ref := new(reference) 282 | ref.foreignKey = fk 283 | ref.model = refModel 284 | ref.refKey = fd.name 285 | if model.refs == nil { 286 | model.refs = make(map[string]*reference) 287 | } 288 | model.refs[refName] = ref 289 | } else if !implicitJoin { 290 | panic("Referenced field is not pointer") 291 | } 292 | } else if !implicitJoin { 293 | panic("Can not find referenced field") 294 | } 295 | } 296 | if fd.unique { 297 | model.indexes.AddUnique(fd.name) 298 | } else if fd.index { 299 | model.indexes.Add(fd.name) 300 | } 301 | } 302 | } 303 | if root { 304 | if indexed, ok := f.(Indexed); ok { 305 | indexed.Indexes(&model.indexes) 306 | } 307 | } 308 | return model 309 | } 310 | 311 | func tableName(talbe interface{}) string { 312 | if t, ok := talbe.(string); ok { 313 | return t 314 | } 315 | t := reflect.TypeOf(talbe).Elem() 316 | for { 317 | c := false 318 | switch t.Kind() { 319 | case reflect.Array, reflect.Chan, reflect.Map, reflect.Ptr, reflect.Slice: 320 | t = t.Elem() 321 | c = true 322 | } 323 | if !c { 324 | break 325 | } 326 | } 327 | if tn, ok := talbe.(TableNamer); ok { 328 | return tn.TableName() 329 | } 330 | return StructNameToTableName(t.Name()) 331 | } 332 | 333 | func parseTags(fd *modelField, s string) { 334 | if s == "" { 335 | return 336 | } 337 | c := strings.Split(s, ",") 338 | for _, v := range c { 339 | c2 := strings.Split(v, ":") 340 | if len(c2) == 2 { 341 | switch c2[0] { 342 | case "fk": 343 | fd.fk = c2[1] 344 | case "size": 345 | fd.size, _ = strconv.Atoi(c2[1]) 346 | case "default": 347 | fd.dfault = c2[1] 348 | case "join": 349 | fd.join = c2[1] 350 | case "coltype": 351 | fd.colType = c2[1] 352 | default: 353 | panic(c2[0] + " tag syntax error") 354 | } 355 | } else { 356 | switch c2[0] { 357 | case "created": 358 | fd.created = true 359 | case "pk": 360 | fd.pk = true 361 | case "updated": 362 | fd.updated = true 363 | case "index": 364 | fd.index = true 365 | case "unique": 366 | fd.unique = true 367 | case "notnull": 368 | fd.notnull = true 369 | default: 370 | panic(c2[0] + " tag syntax error") 371 | } 372 | } 373 | } 374 | return 375 | } 376 | 377 | func toSnake(s string) string { 378 | buf := new(bytes.Buffer) 379 | for i := 0; i < len(s); i++ { 380 | c := s[i] 381 | if c >= 'A' && c <= 'Z' { 382 | if i > 0 { 383 | buf.WriteByte('_') 384 | } 385 | buf.WriteByte(c + 32) 386 | } else { 387 | buf.WriteByte(c) 388 | } 389 | } 390 | return buf.String() 391 | } 392 | 393 | func snakeToUpperCamel(s string) string { 394 | buf := new(bytes.Buffer) 395 | first := true 396 | for i := 0; i < len(s); i++ { 397 | c := s[i] 398 | if c >= 'a' && c <= 'z' && first { 399 | buf.WriteByte(c - 32) 400 | first = false 401 | } else if c == '_' { 402 | first = true 403 | continue 404 | } else { 405 | buf.WriteByte(c) 406 | } 407 | } 408 | return buf.String() 409 | } 410 | 411 | var ValidTags = map[string]bool{ 412 | "pk": true, //primary key 413 | "fk": true, //foreign key 414 | "size": true, 415 | "default": true, 416 | "join": true, 417 | "-": true, //ignore 418 | "index": true, 419 | "unique": true, 420 | "notnull": true, 421 | "updated": true, 422 | "created": true, 423 | "coltype": true, 424 | } 425 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Qbs 2 | === 3 | 4 | Qbs stands for Query By Struct. A Go ORM. [中文版 README](https://github.com/coocood/qbs/blob/master/README_ZH.md) 5 | 6 | [![Build Status](https://drone.io/github.com/coocood/qbs/status.png)](https://drone.io/github.com/coocood/qbs/latest) 7 | 8 | ##ChangeLog 9 | 10 | * 2013.03.14: index name has changed to `{table name}_{column name}`. 11 | - For existing application with existing database, update to this change may lead to creating redundant index, you may need to drop duplicated index manually. 12 | * 2013.03.14: make all internal structures unexported. 13 | * 2013.05.22: fixed memory leak issue. 14 | * 2013.05.23: Breaking change, improved performance up to 100%, removed deprecated methods, make Db and Tx field unexported. 15 | * 2013.05.25: Added `QueryStruct` method to do raw SQL query and fill the result into struct or slice of struct. 16 | Added support for defining custom table/struct and column/field name convertion function, so you are not forced to use snake case in database. 17 | * 2013.05.27: dropped go 1.0 support, only support go 1.1, changed to use build-in sql.DB connection pool. 18 | * 2013.05.29: Added `Iterate` method for processing large data without loading all rows into memory. 19 | * 2013.08.03: Fixed a bug that exceeds max_prepared_stmt_count 20 | 21 | ##Features 22 | 23 | * Define table schema in struct type, create table if not exists. 24 | * Detect table columns in database and alter table add new column automatically. 25 | * Define selection clause in struct type, fields in the struct type become the columns to be selected. 26 | * Define join query in struct type by add pointer fields which point to the parent table's struct. 27 | * Do CRUD query by struct value. 28 | * After a query, all the data you need will be filled into the struct value. 29 | * Compose where clause by condition, which can easily handle complex precedence of "AND/OR" sub conditions. 30 | * If Id value in the struct is provided, it will be added to the where clause. 31 | * "Created" column will be set to current time when insert, "Updated" column will be set to current time when insert and update. 32 | * Struct type can implement Validator interface to do validation before insert or update. 33 | * Support MySQL, PosgreSQL and SQLite3. 34 | * Support connection pool. 35 | 36 | ## Performance 37 | 38 | `Qbs.Find` is about 60% faster on mysql, 130% faster on postgreSQL than raw `Db.Query`, about 20% slower than raw `Stmt.Query`. (benchmarked on windows). 39 | The reason why it is faster than `Db.Query` is because all prepared Statements are cached in map. 40 | 41 | ##Install 42 | 43 | Only support go 1.1+ 44 | 45 | Go get to get the most recent source code. 46 | 47 | go get github.com/coocood/qbs 48 | 49 | New version may break backwards compatibility, so for production project, it's better to 50 | download the tagged version. The most recent release is [v0.2](https://github.com/coocood/qbs/tags). 51 | 52 | tags with same minor version would be backward compatible, e.g `v0.1` and `v0.1.1`. 53 | 54 | tags with different minor version would break compatibility, e.g `v0.1.1` and `v0.2`. 55 | 56 | ## API Documentation 57 | 58 | See [Gowalker](http://gowalker.org/github.com/coocood/qbs) for complete documentation. 59 | 60 | ##Get Started 61 | 62 | ###First you need to register your database 63 | 64 | * The `qbs.Register` function has two more arguments than `sql.Open`, they are database name and dilect instance. 65 | * You only need to call it once at the start time.. 66 | 67 | func RegisterDb(){ 68 | qbs.Register("mysql","qbs_test@/qbs_test?charset=utf8&parseTime=true&loc=Local", "qbs_test", qbs.NewMysql()) 69 | } 70 | 71 | ### Define a model `User` 72 | - If the field name is `Id` and field type is `int64`, the field will be considered as the primary key of the table. 73 | if you want define a primary key with name other than `Id`, you can set the tag `qbs:"pk"` to explictly mark the field as primary key. 74 | - The tag of `Name` field `qbs:"size:32,index"` is used to define the column attributes when create the table, attributes are comma seperated, inside double quotes. 75 | - The `size:32` tag on a string field will be translated to SQL `varchar(32)`, add `index` attribute to create a index on the column, add `unique` attribute to create a unique index on the column 76 | - Some DB (MySQL) can not create a index on string column without `size` defined. 77 | 78 | type User struct { 79 | Id int64 80 | Name string `qbs:"size:32,index"` 81 | } 82 | 83 | - If you want to create multi column index, you should implement `Indexed` interface by define a `Indexes` method like the following. 84 | 85 | func (*User) Indexes(indexes *qbs.Indexes){ 86 | //indexes.Add("column_a", "column_b") or indexes.AddUnique("column_a", "column_b") 87 | } 88 | 89 | ###Create a new table 90 | 91 | - call `qbs.GetMigration` function to get a Migration instance, and then use it to create a table. 92 | - When you create a table, if the table already exists, it will not recreate it, but looking for newly added columns or indexes in the model, and execute add column or add index operation. 93 | - It is better to do create table task at the start time, because the Migration only do incremental operation, it is safe to keep the table creation code in production enviroment. 94 | - `CreateTableIfNotExists` expect a struct pointer parameter. 95 | 96 | func CreateUserTable() error{ 97 | migration, err := qbs.GetMigration() 98 | if err != nil { 99 | return err 100 | } 101 | defer migration.Close() 102 | return migration.CreateTableIfNotExists(new(User)) 103 | } 104 | 105 | ### Get and use `*qbs.Qbs` instance: 106 | - Suppose we are in a handle http function. call `qbs.GetQbs()` to get a instance. 107 | - Be sure to close it by calling `defer q.Close()` after get it. 108 | - qbs has connection pool, the default size is 100, you can call `qbs.ChangePoolSize()` to change the size. 109 | 110 | func GetUser(w http.ResponseWriter, r *http.Request){ 111 | q, err := qbs.GetQbs() 112 | if err != nil { 113 | fmt.Println(err) 114 | w.WriteHeader(500) 115 | return 116 | } 117 | defer q.Close() 118 | u, err := FindUserById(q, 6) 119 | data, _ := json.Marshal(u) 120 | w.Write(data) 121 | } 122 | 123 | ### Inset a row: 124 | - Call `Save` method to insert or update the row,if the primary key field `Id` has not been set, `Save` would execute insert stamtment. 125 | - If `Id` is set to a positive integer, `Save` would query the count of the row to find out if the row already exists, if not then execute `INSERT` statement. 126 | otherwise execute `UPDATE`. 127 | - `Save` expects a struct pointer parameter. 128 | 129 | func CreateUser(q *qbs.Qbs) (*User,error){ 130 | user := new(User) 131 | user.Name = "Green" 132 | _, err := q.Save(user) 133 | return user,err 134 | } 135 | 136 | ### Find: 137 | - If you want to get a row by `Id`, just assign the `Id` value to the model instance. 138 | 139 | func FindUserById(q *qbs.Qbs, id int64) (*User, error) { 140 | user := new(User) 141 | user.Id = id 142 | err := q.Find(user) 143 | return user, err 144 | } 145 | 146 | - Call `FindAll` to get multiple rows, it expects a pointer of slice, and the element of the slice must be a pointer of struct. 147 | 148 | func FindUsers(q *qbs.Qbs) ([]*User, error) { 149 | var users []*User 150 | err := q.Limit(10).Offset(10).FindAll(&users) 151 | return users, err 152 | } 153 | 154 | - If you want to add conditions other than `Id`, you should all `Where` method. `WhereEqual("name", name)` is equivalent to `Where("name = ?", name)`, just a shorthand method. 155 | - Only the last call to `Where`/`WhereEqual` counts, so it is only applicable to define simple condition. 156 | - Notice that the column name passed to `WhereEqual` method is lower case, by default, all the camel case field name and struct name will be converted to snake case in database storage, 157 | so whenever you pass a column name or table name parameter in string, it should be in snake case. 158 | - You can change the convertion behavior by setting the 4 convertion function variable: `FieldNameToColumnName`,`StructNameToTableName`,`ColumnNameToFieldName`,`TableNameToStructName` to your own function. 159 | 160 | func FindUserByName(q *qbs.Qbs, n string) (*User, error) { 161 | user := new(User) 162 | err := q.WhereEqual("name", n).Find(user) 163 | return user, err 164 | } 165 | 166 | - If you need to define more complex condition, you should call `Condition` method, it expects a `*Condition` parameter. 167 | you can get a new condition instance by calling `qbs.NewCondition`, `qbs.NewEqualCondition` or `qbs.NewInCondition` function. 168 | - `*Condition` instance has `And`, `Or` ... methods, can be called sequentially to construct a complex condition. 169 | - `Condition` method of Qbs instance should only be called once as well, it will replace previous condition defined by `Condition` or `Where` methods. 170 | 171 | func FindUserByCondition(q *qbs.Qbs) (*User, error) { 172 | user := new(User) 173 | condition1 := qbs.NewCondition("id > ?", 100).Or("id < ?", 50).OrEqual("id", 75) 174 | condition2 := qbs.NewCondition("name != ?", "Red").And("name != ?", "Black") 175 | condition1.AndCondition(condition2) 176 | err := q.Condition(condition1).Find(user) 177 | return user, err 178 | } 179 | 180 | ### Update a single row 181 | - To update a single row, you should call `Find` first, then update the model, and `Save` it. 182 | 183 | func UpdateOneUser(q *qbs.Qbs, id int64, name string) (affected int64, error){ 184 | user, err := FindUserById(q, id) 185 | if err != nil { 186 | return 0, err 187 | } 188 | user.Name = name 189 | return q.Save(user) 190 | } 191 | 192 | ### Update multiple row 193 | - Call `Update` to update multiple rows at once, but you should call this method cautiously, if the the model struct contains all the columns, it will update every column, most of the time this is not what we want. 194 | - The right way to do it is to define a temporary model struct in method or block, that only contains the column we want to update. 195 | 196 | func UpdateMultipleUsers(q *qbs.Qbs)(affected int64, error) { 197 | type User struct { 198 | Name string 199 | } 200 | user := new(User) 201 | user.Name = "Blue" 202 | return q.WhereEqual("name", "Green").Update(user) 203 | } 204 | 205 | ### Delete 206 | - call `Delete` method to delete a row, there must be at least one condition defined, either by `Id` value, or by `Where`/`Condition`. 207 | 208 | func DeleteUser(q *qbs.Qbs, id int64)(affected int64, err error) { 209 | user := new(User) 210 | user.Id = id 211 | return q.Delete(user) 212 | } 213 | 214 | ### Define another table for join query 215 | - For join query to work, you should has a pair of fields to define the join relationship in the model struct. 216 | - Here the model `Post` has a `AuthorId` int64 field, and has a `Author` field of type `*User`. 217 | - The rule to define join relationship is like `{xxx}Id int64`, `{xxx} *{yyy}`. 218 | - As the `Author` field is pointer type, it will be ignored when creating table. 219 | - As `AuthorId` is a join column, a index of it will be created automatically when creating the table, so you don't have to add `qbs:"index"` tag on it. 220 | - You can also set the join column explicitly by add a tag `qbs:"join:Author"` to it for arbitrary field Name. here `Author` is the struct pointer field of the parent table model. 221 | - To define a foreign key constraint, you have to explicitly add a tag `qbs:"fk:Author"` to the foreign key column, and an index will be created as well when creating table. 222 | - `Created time.Time` field will be set to the current time when insert a row,`Updated time.Time` field will be set to current time when update the row. 223 | - You can explicitly set tag `qbs:"created"` or `qbs:"updated"` on `time.Time` field to get the functionality for arbitrary field name. 224 | 225 | type Post struct { 226 | Id int64 227 | AuthorId int64 228 | Author *User 229 | Content string 230 | Created time.Time 231 | Updated time.Time 232 | } 233 | 234 | ### Omit some column 235 | - Sometimes we do not need to get every field of a model, especially for joined field (like `Author` field) or large field (like `Content` field). 236 | - Omit them will get better performance. 237 | 238 | func FindPostsOmitContentAndCreated(q *qbs.Qbs) ([]*Post, error) { 239 | var posts []*Post 240 | err := q.OmitFields("Content","Created").Find(&posts) 241 | return posts, err 242 | } 243 | 244 | - With `OmitJoin`, you can omit every join fields, return only the columns in a single table, and it can be used along with `OmitFields`. 245 | 246 | func FindPostsOmitJoin(q *qbs.Qbs) ([]*Post, error) { 247 | var posts []*Post 248 | err := q.OmitJoin().OmitFields("Content").Find(&posts) 249 | return posts, err 250 | } 251 | 252 | ##Projects use Qbs: 253 | 254 | - a CMS system [toropress](https://github.com/insionng/toropress) 255 | - Go documentation reference website [Gowalker](http://gowalker.org/) 256 | - Nebri OS (https://nebrios.com/) 257 | 258 | ##Contributors 259 | [Erik Aigner](https://github.com/eaigner) 260 | Qbs was originally a fork from [hood](https://github.com/eaigner/hood) by [Erik Aigner](https://github.com/eaigner), 261 | but I changed more than 80% of the code, then it ended up become a totally different ORM. 262 | 263 | [NuVivo314](https://github.com/NuVivo314), [Jason McVetta](https://github.com/jmcvetta), [pix64](https://github.com/pix64), [vadimi](https://github.com/vadimi), [Ravi Teja](https://github.com/tejainece). 264 | -------------------------------------------------------------------------------- /db_test_util.go: -------------------------------------------------------------------------------- 1 | package qbs 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "time" 7 | ) 8 | 9 | var testDbName = "qbs_test" 10 | var testDbUser = "qbs_test" 11 | 12 | type addColumn struct { 13 | Prim int64 `qbs:"pk"` 14 | First string `qbs:"size:64,notnull"` 15 | Last string `qbs:"size:128,default:'defaultValue'"` 16 | Amount int 17 | } 18 | 19 | type fakeInt int 20 | type fakeInt16 int16 21 | type fakeBool bool 22 | type fakeFloat float32 23 | type fakeTime time.Time 24 | type fakeString string 25 | 26 | type typeTestTable struct { 27 | Bool bool `qbs:""` 28 | 29 | Int8 int8 `qbs:""` 30 | Int16 int16 `qbs:""` 31 | Int32 int32 `qbs:""` 32 | UInt8 uint8 `qbs:""` 33 | UInt16 uint16 `qbs:""` 34 | UInt32 uint32 `qbs:""` 35 | 36 | Int int `qbs:""` 37 | UInt uint `qbs:""` 38 | Int64 int64 `qbs:""` 39 | UInt64 uint64 `qbs:""` 40 | 41 | Float32 float32 42 | Float64 float64 43 | 44 | Varchar string `qbs:"size:128"` 45 | LongText string `qbs:"size:65536"` 46 | 47 | Time time.Time 48 | 49 | Slice []byte 50 | 51 | DerivedInt fakeInt `qbs:"coltype:int"` 52 | DerivedInt16 fakeInt16 `qbs:"coltype:bigint"` 53 | DerivedBool fakeBool `qbs:"coltype:boolean"` 54 | DerivedFloat fakeFloat `qbs:"coltype:double"` 55 | DerivedTime fakeTime `qbs:"coltype:timestamp"` 56 | DerivedVarChar fakeTime `qbs:"coltype:text,size:128"` 57 | DerivedLongText fakeTime `qbs:"coltype:text,size:65536"` 58 | } 59 | 60 | func (table *addColumn) Indexes(indexes *Indexes) { 61 | indexes.AddUnique("first", "last") 62 | } 63 | 64 | func doTestTransaction(assert *Assert) { 65 | type txModel struct { 66 | Id int64 67 | A string 68 | } 69 | table := txModel{ 70 | A: "A", 71 | } 72 | err := WithMigration(func(mg *Migration) error { 73 | mg.dropTableIfExists(&table) 74 | mg.CreateTableIfNotExists(&table) 75 | return nil 76 | }) 77 | assert.MustNil(err) 78 | WithQbs(func(q *Qbs) error { 79 | q.Begin() 80 | assert.NotNil(q.tx) 81 | _, err := q.Save(&table) 82 | assert.Nil(err) 83 | err = q.Rollback() 84 | assert.Nil(err) 85 | out := new(txModel) 86 | err = q.Find(out) 87 | assert.Equal(sql.ErrNoRows, err) 88 | q.Begin() 89 | table.Id = 0 90 | _, err = q.Save(&table) 91 | assert.Nil(err) 92 | err = q.Commit() 93 | assert.Nil(err) 94 | out.Id = table.Id 95 | err = q.Find(out) 96 | assert.Nil(err) 97 | assert.Equal("A", out.A) 98 | return nil 99 | }) 100 | 101 | } 102 | 103 | func doTestSaveAndDelete(assert *Assert, mg *Migration, q *Qbs) { 104 | defer closeMigrationAndQbs(mg, q) 105 | x := time.Now() 106 | assert.Equal(0, x.Sub(x.UTC())) 107 | now := time.Now() 108 | type saveModel struct { 109 | Id int64 110 | A string 111 | B int 112 | Updated time.Time 113 | Created time.Time 114 | } 115 | model1 := saveModel{ 116 | A: "banana", 117 | B: 5, 118 | } 119 | model2 := saveModel{ 120 | A: "orange", 121 | B: 4, 122 | } 123 | 124 | mg.dropTableIfExists(&model1) 125 | mg.CreateTableIfNotExists(&model1) 126 | 127 | affected, err := q.Save(&model1) 128 | assert.MustNil(err) 129 | assert.Equal(1, affected) 130 | assert.True(model1.Created.Sub(now) > 0) 131 | assert.True(model1.Updated.Sub(now) > 0) 132 | // make sure created/updated values match the db 133 | var model1r []*saveModel 134 | err = q.WhereEqual("id", model1.Id).FindAll(&model1r) 135 | 136 | assert.MustNil(err) 137 | assert.MustEqual(1, len(model1r)) 138 | assert.Equal(model1.Created.Unix(), model1r[0].Created.Unix()) 139 | assert.Equal(model1.Updated.Unix(), model1r[0].Updated.Unix()) 140 | 141 | oldCreate := model1.Created 142 | oldUpdate := model1.Updated 143 | model1.A = "grape" 144 | model1.B = 9 145 | 146 | time.Sleep(time.Second * 1) // sleep for 1 sec 147 | affected, err = q.Save(&model1) 148 | assert.MustNil(err) 149 | assert.MustEqual(1, affected) 150 | assert.True(model1.Created.Equal(oldCreate)) 151 | assert.True(model1.Updated.Sub(oldUpdate) > 0) 152 | 153 | // make sure created/updated values match the db 154 | var model1r2 []*saveModel 155 | err = q.Where("id = ?", model1.Id).FindAll(&model1r2) 156 | assert.MustNil(err) 157 | assert.MustEqual(1, len(model1r2)) 158 | assert.True(model1r2[0].Updated.Sub(model1r2[0].Created) >= 1) 159 | assert.Equal(model1.Created.Unix(), model1r2[0].Created.Unix()) 160 | assert.Equal(model1.Updated.Unix(), model1r2[0].Updated.Unix()) 161 | 162 | affected, err = q.Save(&model2) 163 | assert.MustNil(err) 164 | assert.Equal(1, affected) 165 | 166 | affected, err = q.Delete(&model2) 167 | assert.MustNil(err) 168 | assert.Equal(1, affected) 169 | } 170 | 171 | func doTestSaveAgain(assert *Assert, mg *Migration, q *Qbs) { 172 | defer closeMigrationAndQbs(mg, q) 173 | b := new(basic) 174 | mg.dropTableIfExists(b) 175 | mg.CreateTableIfNotExists(b) 176 | b.Name = "a" 177 | b.State = 2 178 | affected, err := q.Save(b) 179 | assert.Nil(err) 180 | assert.Equal(1, affected) 181 | affected, err = q.Save(b) 182 | assert.Nil(err) 183 | if _, ok := q.Dialect.(*mysql); ok { 184 | assert.Equal(0, affected) 185 | } else { 186 | assert.Equal(1, affected) 187 | } 188 | } 189 | 190 | func doTestForeignKey(assert *Assert) { 191 | type User struct { 192 | Id int64 193 | Name string 194 | } 195 | type Post struct { 196 | Id int64 197 | Title string 198 | AuthorId int64 199 | Author *User 200 | } 201 | aUser := &User{ 202 | Name: "john", 203 | } 204 | aPost := &Post{ 205 | Title: "A Title", 206 | } 207 | WithMigration(func(mg *Migration) error { 208 | mg.dropTableIfExists(aPost) 209 | mg.dropTableIfExists(aUser) 210 | mg.CreateTableIfNotExists(aUser) 211 | mg.CreateTableIfNotExists(aPost) 212 | return nil 213 | }) 214 | WithQbs(func(q *Qbs) error { 215 | affected, err := q.Save(aUser) 216 | assert.Nil(err) 217 | aPost.AuthorId = int64(aUser.Id) 218 | affected, err = q.Save(aPost) 219 | assert.Equal(1, affected) 220 | pst := new(Post) 221 | pst.Id = aPost.Id 222 | err = q.Find(pst) 223 | assert.MustNil(err) 224 | assert.Equal(aPost.Id, pst.Id) 225 | assert.Equal("john", pst.Author.Name) 226 | 227 | pst.Author = nil 228 | err = q.OmitFields("Author").Find(pst) 229 | assert.MustNil(err) 230 | assert.MustNil(pst.Author) 231 | 232 | err = q.OmitJoin().Find(pst) 233 | assert.MustNil(err) 234 | assert.MustNil(pst.Author) 235 | 236 | var psts []*Post 237 | err = q.FindAll(&psts) 238 | assert.MustNil(err) 239 | assert.MustEqual(1, len(psts)) 240 | assert.Equal("john", psts[0].Author.Name) 241 | return nil 242 | }) 243 | } 244 | 245 | func doTestFind(assert *Assert) { 246 | now := time.Now() 247 | type types struct { 248 | Id int64 249 | Str string 250 | Intgr int64 251 | Flt float64 252 | Bytes []byte 253 | Time time.Time 254 | } 255 | modelData := &types{ 256 | Str: "string!", 257 | Intgr: -1, 258 | Flt: 3.8, 259 | Bytes: []byte("bytes!"), 260 | Time: now, 261 | } 262 | WithMigration(func(mg *Migration) error { 263 | mg.dropTableIfExists(modelData) 264 | mg.CreateTableIfNotExists(modelData) 265 | return nil 266 | }) 267 | WithQbs(func(q *Qbs) error { 268 | out := new(types) 269 | condition := NewCondition("str = ?", "string!").And("intgr = ?", -1) 270 | err := q.Condition(condition).Find(out) 271 | assert.Equal(sql.ErrNoRows, err) 272 | 273 | affected, err := q.Save(modelData) 274 | assert.Nil(err) 275 | assert.Equal(1, affected) 276 | out.Id = modelData.Id 277 | err = q.Condition(condition).Find(out) 278 | assert.Nil(err) 279 | assert.Equal(1, out.Id) 280 | assert.Equal("string!", out.Str) 281 | assert.Equal(-1, out.Intgr) 282 | assert.Equal(3.8, out.Flt) 283 | assert.Equal([]byte("bytes!"), out.Bytes) 284 | diff := now.Sub(out.Time) 285 | assert.True(diff < time.Second && diff > -time.Second) 286 | 287 | modelData.Id = 5 288 | modelData.Str = "New row" 289 | _, err = q.Save(modelData) 290 | assert.Nil(err) 291 | 292 | out = new(types) 293 | condition = NewCondition("str = ?", "New row").And("flt = ?", 3.8) 294 | err = q.Condition(condition).Find(out) 295 | assert.Nil(err) 296 | assert.Equal(5, out.Id) 297 | 298 | out = new(types) 299 | out.Id = 100 300 | err = q.Find(out) 301 | assert.NotNil(err) 302 | 303 | allOut := []*types{} 304 | err = q.WhereEqual("intgr", -1).FindAll(&allOut) 305 | assert.Nil(err) 306 | assert.Equal(2, len(allOut)) 307 | return nil 308 | }) 309 | } 310 | 311 | func doTestCreateTable(assert *Assert, mg *Migration) { 312 | defer mg.Close() 313 | { 314 | type AddColumn struct { 315 | Prim int64 `qbs:"pk"` 316 | } 317 | table := &AddColumn{} 318 | mg.dropTableIfExists(table) 319 | mg.CreateTableIfNotExists(table) 320 | columns := mg.dialect.columnsInTable(mg, table) 321 | assert.Equal(1, len(columns)) 322 | assert.True(columns["prim"]) 323 | } 324 | table := &addColumn{} 325 | mg.CreateTableIfNotExists(table) 326 | assert.True(mg.dialect.indexExists(mg, "add_column", "add_column_first_last")) 327 | columns := mg.dialect.columnsInTable(mg, table) 328 | assert.Equal(4, len(columns)) 329 | 330 | { 331 | tableWithCustomTypes := new(typeTestTable) 332 | mg.dropTableIfExists(tableWithCustomTypes) 333 | mg.CreateTableIfNotExists(tableWithCustomTypes) 334 | columns := mg.dialect.columnsInTable(mg, tableWithCustomTypes) 335 | assert.Equal(24, len(columns)) 336 | assert.True(columns["derived_int"]) 337 | assert.True(columns["derived_int16"]) 338 | assert.True(columns["derived_bool"]) 339 | assert.True(columns["derived_float"]) 340 | assert.True(columns["derived_time"]) 341 | assert.True(columns["derived_var_char"]) 342 | assert.True(columns["derived_long_text"]) 343 | } 344 | } 345 | 346 | type basic struct { 347 | Id int64 348 | Name string `qbs:"size:64"` 349 | State int64 350 | } 351 | 352 | func doTestUpdate(assert *Assert, mg *Migration, q *Qbs) { 353 | defer closeMigrationAndQbs(mg, q) 354 | mg.dropTableIfExists(&basic{}) 355 | mg.CreateTableIfNotExists(&basic{}) 356 | _, err := q.Save(&basic{Name: "a", State: 1}) 357 | _, err = q.Save(&basic{Name: "b", State: 1}) 358 | _, err = q.Save(&basic{Name: "c", State: 0}) 359 | assert.MustNil(err) 360 | { 361 | // define a temporary struct in a block to update partial columns of a table 362 | // as the type is in a block, so it will not conflict with other types with the same name in the same method 363 | type basic struct { 364 | Name string 365 | } 366 | affected, err := q.WhereEqual("state", 1).Update(&basic{Name: "d"}) 367 | assert.MustNil(err) 368 | assert.Equal(2, affected) 369 | 370 | var datas []*basic 371 | q.WhereEqual("state", 1).FindAll(&datas) 372 | assert.MustEqual(2, len(datas)) 373 | assert.Equal("d", datas[0].Name) 374 | assert.Equal("d", datas[1].Name) 375 | } 376 | 377 | // if choose basic table type to update, all zero value in the struct will be updated too. 378 | // this may be cause problems, so define a temporary struct to update table is the recommended way. 379 | affected, err := q.Where("state = ?", 1).Update(&basic{Name: "e"}) 380 | assert.MustNil(err) 381 | assert.Equal(2, affected) 382 | var datas []*basic 383 | q.WhereEqual("state", 1).FindAll(&datas) 384 | assert.MustEqual(0, len(datas)) 385 | } 386 | 387 | type validatorTable struct { 388 | Id int64 389 | Name string 390 | } 391 | 392 | func (v *validatorTable) Validate(q *Qbs) error { 393 | if q.ContainsValue(v, "name", v.Name) { 394 | return errors.New("name already taken") 395 | } 396 | return nil 397 | } 398 | 399 | func doTestValidation(assert *Assert, mg *Migration, q *Qbs) { 400 | defer closeMigrationAndQbs(mg, q) 401 | valid := new(validatorTable) 402 | mg.dropTableIfExists(valid) 403 | mg.CreateTableIfNotExists(valid) 404 | valid.Name = "ok" 405 | q.Save(valid) 406 | valid.Id = 0 407 | _, err := q.Save(valid) 408 | assert.MustNotNil(err) 409 | assert.Equal("name already taken", err.Error()) 410 | } 411 | 412 | func doTestBoolType(assert *Assert, mg *Migration, q *Qbs) { 413 | defer closeMigrationAndQbs(mg, q) 414 | type BoolType struct { 415 | Id int64 416 | Active bool 417 | } 418 | bt := new(BoolType) 419 | mg.dropTableIfExists(bt) 420 | mg.CreateTableIfNotExists(bt) 421 | bt.Active = true 422 | q.Save(bt) 423 | bt.Active = false 424 | q.WhereEqual("active", true).Find(bt) 425 | assert.True(bt.Active) 426 | } 427 | 428 | func doTestStringPk(assert *Assert, mg *Migration, q *Qbs) { 429 | defer closeMigrationAndQbs(mg, q) 430 | type StringPk struct { 431 | Tag string `qbs:"pk,size:16"` 432 | Count int32 433 | } 434 | spk := new(StringPk) 435 | spk.Tag = "health" 436 | spk.Count = 10 437 | mg.dropTableIfExists(spk) 438 | mg.CreateTableIfNotExists(spk) 439 | affected, _ := q.Save(spk) 440 | assert.Equal(1, affected) 441 | spk.Count = 0 442 | q.Find(spk) 443 | assert.Equal(10, spk.Count) 444 | } 445 | 446 | func doTestCount(assert *Assert) { 447 | setupBasicDb() 448 | WithQbs(func(q *Qbs) error { 449 | basic := new(basic) 450 | basic.Name = "name" 451 | basic.State = 1 452 | q.Save(basic) 453 | for i := 0; i < 5; i++ { 454 | basic.Id = 0 455 | basic.State = 2 456 | q.Save(basic) 457 | } 458 | count1 := q.Count("basic") 459 | assert.Equal(6, count1) 460 | count2 := q.WhereEqual("state", 2).Count(basic) 461 | assert.Equal(5, count2) 462 | return nil 463 | }) 464 | } 465 | 466 | func doTestQueryMap(assert *Assert, mg *Migration, q *Qbs) { 467 | defer closeMigrationAndQbs(mg, q) 468 | type types struct { 469 | Id int64 470 | Name string `qbs:"size:64"` 471 | Created time.Time 472 | } 473 | tp := new(types) 474 | mg.dropTableIfExists(tp) 475 | mg.CreateTableIfNotExists(tp) 476 | result, err := q.QueryMap("SELECT * FROM types") 477 | assert.Nil(result) 478 | assert.Equal(sql.ErrNoRows, err) 479 | for i := 0; i < 3; i++ { 480 | tp.Id = 0 481 | tp.Name = "abc" 482 | q.Save(tp) 483 | } 484 | result, err = q.QueryMap("SELECT * FROM types") 485 | assert.NotNil(result) 486 | assert.Equal(1, result["id"]) 487 | assert.Equal("abc", result["name"]) 488 | if _, sql3 := q.Dialect.(*sqlite3); !sql3 { 489 | _, ok := result["created"].(time.Time) 490 | assert.True(ok) 491 | } else { 492 | _, ok := result["created"].(string) 493 | assert.True(ok) 494 | } 495 | results, err := q.QueryMapSlice("SELECT * FROM types") 496 | assert.Equal(3, len(results)) 497 | } 498 | 499 | func doTestBulkInsert(assert *Assert) { 500 | setupBasicDb() 501 | WithQbs(func(q *Qbs) error { 502 | var bulk []*basic 503 | for i := 0; i < 10; i++ { 504 | b := new(basic) 505 | b.Name = "basic" 506 | b.State = int64(i) 507 | bulk = append(bulk, b) 508 | } 509 | err := q.BulkInsert(bulk) 510 | assert.Nil(err) 511 | for i := 0; i < 10; i++ { 512 | assert.Equal(i+1, bulk[i].Id) 513 | } 514 | return nil 515 | }) 516 | } 517 | 518 | func doTestQueryStruct(assert *Assert) { 519 | setupBasicDb() 520 | WithQbs(func(q *Qbs) error { 521 | b := new(basic) 522 | b.Name = "abc" 523 | b.State = 2 524 | q.Save(b) 525 | b = new(basic) 526 | err := q.QueryStruct(b, "SELECT * FROM basic") 527 | assert.Nil(err) 528 | assert.Equal(1, b.Id) 529 | assert.Equal("abc", b.Name) 530 | assert.Equal(2, b.State) 531 | var slice []*basic 532 | q.QueryStruct(&slice, "SELECT * FROM basic") 533 | assert.Equal(1, len(slice)) 534 | assert.Equal("abc", slice[0].Name) 535 | return nil 536 | }) 537 | } 538 | 539 | func doTestConnectionLimit(assert *Assert) { 540 | SetConnectionLimit(2, false) 541 | q0, _ := GetQbs() 542 | GetQbs() 543 | GetQbs() 544 | _, err := GetQbs() 545 | assert.Equal(ConnectionLimitError, err) 546 | q0.Close() 547 | q4, _ := GetQbs() 548 | assert.NotNil(q4) 549 | SetConnectionLimit(0, true) 550 | a := 0 551 | go func() { 552 | a = 1 553 | q4.Close() 554 | }() 555 | GetQbs() 556 | assert.Equal(1, a) 557 | SetConnectionLimit(-1, false) 558 | assert.Nil(connectionLimit) 559 | } 560 | 561 | func doTestIterate(assert *Assert) { 562 | setupBasicDb() 563 | q, _ := GetQbs() 564 | for i := 0; i < 4; i++ { 565 | b := new(basic) 566 | b.State = int64(i) 567 | q.Save(b) 568 | } 569 | var stateSum int64 570 | b := new(basic) 571 | err := q.Iterate(b, func() error { 572 | if b.State == 3 { 573 | return errors.New("A error") 574 | } 575 | stateSum += b.State 576 | return nil 577 | }) 578 | assert.Equal("A error", err.Error()) 579 | assert.Equal(3, stateSum) 580 | } 581 | 582 | func setupBasicDb() { 583 | WithMigration(func(mg *Migration) error { 584 | b := new(basic) 585 | mg.dropTableIfExists(b) 586 | mg.CreateTableIfNotExists(b) 587 | return nil 588 | }) 589 | } 590 | 591 | func closeMigrationAndQbs(mg *Migration, q *Qbs) { 592 | mg.Close() 593 | q.Close() 594 | } 595 | 596 | func noConvert(s string) string { 597 | return s 598 | } 599 | 600 | func doTestSaveNullable(assert *Assert, mg *Migration, q *Qbs) { 601 | defer closeMigrationAndQbs(mg, q) 602 | type nullable struct { 603 | Id int64 604 | Name *string 605 | Age *int64 606 | } 607 | var n nullable 608 | mg.dropTableIfExists(&n) 609 | mg.CreateTableIfNotExists(&n) 610 | 611 | n.Id = 0 612 | n.Name = nil 613 | n.Age = nil 614 | 615 | _, err := q.Save(&n) 616 | if err != nil { 617 | panic(err) 618 | } 619 | 620 | //try to read it back, leave n.Id 621 | 622 | if err := q.Find(&n); err != nil { 623 | panic(err) 624 | } 625 | assert.Nil(n.Name) 626 | assert.Nil(n.Age) 627 | 628 | foo := "foo" 629 | num := int64(99) 630 | 631 | n.Id = 0 632 | n.Name = &foo 633 | n.Age = &num 634 | 635 | _, err = q.Save(&n) 636 | if err != nil { 637 | panic(err) 638 | } 639 | 640 | //did not change the id, because we want to find it 641 | n.Age = nil 642 | n.Name = nil 643 | if err := q.Find(&n); err != nil { 644 | panic(err) 645 | } 646 | assert.NotNil(n.Name) 647 | assert.NotNil(n.Age) 648 | assert.Equal(*n.Name, "foo") 649 | assert.Equal(*n.Age, 99) 650 | } 651 | -------------------------------------------------------------------------------- /qbs.go: -------------------------------------------------------------------------------- 1 | package qbs 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "os" 9 | "reflect" 10 | "strings" 11 | "sync" 12 | "time" 13 | ) 14 | 15 | var driver, driverSource, dbName string 16 | var dial Dialect 17 | var connectionLimit chan struct{} 18 | var blockingOnLimit bool 19 | var ConnectionLimitError = errors.New("Connection limit reached") 20 | var db *sql.DB 21 | var stmtMap map[string]*sql.Stmt 22 | var mu *sync.RWMutex 23 | var queryLogger *log.Logger = log.New(os.Stdout, "qbs:", log.LstdFlags) 24 | var errorLogger *log.Logger = log.New(os.Stderr, "qbs:", log.LstdFlags) 25 | 26 | type Qbs struct { 27 | Dialect Dialect 28 | Log bool //Set to true to print out sql statement. 29 | tx *sql.Tx 30 | txStmtMap map[string]*sql.Stmt 31 | criteria *criteria 32 | firstTxError error 33 | } 34 | 35 | type Validator interface { 36 | Validate(*Qbs) error 37 | } 38 | 39 | //Register a database, should be call at the beginning of the application. 40 | func Register(driverName, driverSourceName, databaseName string, dialect Dialect) { 41 | driverSource = driverSourceName 42 | dbName = databaseName 43 | if db == nil { 44 | var err error 45 | var database *sql.DB 46 | database, err = sql.Open(driverName, driverSource) 47 | if err != nil { 48 | panic(err) 49 | } 50 | RegisterWithDb(driverName, database, dialect) 51 | } 52 | } 53 | 54 | func RegisterWithDb(driverName string, database *sql.DB, dialect Dialect) { 55 | driver = driverName 56 | dial = dialect 57 | db = database 58 | db.SetMaxIdleConns(100) 59 | stmtMap = make(map[string]*sql.Stmt) 60 | mu = new(sync.RWMutex) 61 | } 62 | 63 | //A safe and easy way to work with *Qbs instance without the need to open and close it. 64 | func WithQbs(task func(*Qbs) error) error { 65 | q, err := GetQbs() 66 | if err != nil { 67 | return err 68 | } 69 | defer q.Close() 70 | return task(q) 71 | } 72 | 73 | //Get an Qbs instance, should call `defer q.Close()` next, like: 74 | // 75 | // q, err := qbs.GetQbs() 76 | // if err != nil { 77 | // fmt.Println(err) 78 | // return 79 | // } 80 | // defer q.Close() 81 | // ... 82 | // 83 | func GetQbs() (q *Qbs, err error) { 84 | if driver == "" || dial == nil { 85 | panic("database driver has not been registered, should call Register first.") 86 | } 87 | if connectionLimit != nil { 88 | if blockingOnLimit { 89 | connectionLimit <- struct{}{} 90 | } else { 91 | select { 92 | case connectionLimit <- struct{}{}: 93 | default: 94 | return nil, ConnectionLimitError 95 | } 96 | } 97 | } 98 | q = new(Qbs) 99 | q.Dialect = dial 100 | q.criteria = new(criteria) 101 | return q, nil 102 | } 103 | 104 | //The default connection pool size is 100. 105 | func ChangePoolSize(size int) { 106 | db.SetMaxIdleConns(size) 107 | } 108 | 109 | func SetLogger(query *log.Logger, err *log.Logger) { 110 | queryLogger = query 111 | errorLogger = err 112 | } 113 | 114 | //Set the connection limit, there is no limit by default. 115 | //If blocking is true, GetQbs method will be blocked, otherwise returns ConnectionLimitError. 116 | func SetConnectionLimit(maxCon int, blocking bool) { 117 | if maxCon > 0 { 118 | connectionLimit = make(chan struct{}, maxCon) 119 | } else if maxCon < 0 { 120 | connectionLimit = nil 121 | } 122 | blockingOnLimit = blocking 123 | } 124 | 125 | // Create a new criteria for subsequent query 126 | func (q *Qbs) Reset() { 127 | q.criteria = new(criteria) 128 | } 129 | 130 | // Begin create a transaction object internally 131 | // You can perform queries with the same Qbs object 132 | // no matter it is in transaction or not. 133 | // It panics if it's already in a transaction. 134 | func (q *Qbs) Begin() error { 135 | if q.tx != nil { 136 | panic("cannot start nested transaction") 137 | } 138 | tx, err := db.Begin() 139 | q.tx = tx 140 | q.txStmtMap = make(map[string]*sql.Stmt) 141 | return err 142 | } 143 | 144 | func (q *Qbs) InTransaction() bool { 145 | return q.tx != nil 146 | } 147 | 148 | func (q *Qbs) updateTxError(e error) error { 149 | if e != nil { 150 | if errorLogger != nil { 151 | errorLogger.Println(e) 152 | } 153 | // don't shadow the first error 154 | if q.firstTxError == nil { 155 | q.firstTxError = e 156 | } 157 | } 158 | return e 159 | } 160 | 161 | // Commit commits a started transaction and will report the first error that 162 | // occurred inside the transaction. 163 | func (q *Qbs) Commit() error { 164 | err := q.tx.Commit() 165 | q.updateTxError(err) 166 | q.tx = nil 167 | for _, v := range q.txStmtMap { 168 | v.Close() 169 | } 170 | q.txStmtMap = nil 171 | return q.firstTxError 172 | } 173 | 174 | // Rollback rolls back a started transaction. 175 | func (q *Qbs) Rollback() error { 176 | err := q.tx.Rollback() 177 | q.tx = nil 178 | for _, v := range q.txStmtMap { 179 | v.Close() 180 | } 181 | q.txStmtMap = nil 182 | return q.updateTxError(err) 183 | } 184 | 185 | // Where is a shortcut method to call Condtion(NewCondtition(expr, args...)). 186 | func (q *Qbs) Where(expr string, args ...interface{}) *Qbs { 187 | q.criteria.condition = NewCondition(expr, args...) 188 | return q 189 | } 190 | 191 | //Snakecase column name 192 | func (q *Qbs) WhereEqual(column string, value interface{}) *Qbs { 193 | q.criteria.condition = NewEqualCondition(column, value) 194 | return q 195 | } 196 | 197 | func (q *Qbs) WhereIn(column string, values []interface{}) *Qbs { 198 | q.criteria.condition = NewInCondition(column, values) 199 | return q 200 | } 201 | 202 | //Condition defines the SQL "WHERE" clause 203 | //If other condition can be inferred by the struct argument in 204 | //Find method, it will be merged with AND 205 | func (q *Qbs) Condition(condition *Condition) *Qbs { 206 | q.criteria.condition = condition 207 | return q 208 | } 209 | 210 | func (q *Qbs) Limit(limit int) *Qbs { 211 | q.criteria.limit = limit 212 | return q 213 | } 214 | 215 | func (q *Qbs) Offset(offset int) *Qbs { 216 | q.criteria.offset = offset 217 | return q 218 | } 219 | 220 | func (q *Qbs) OrderBy(path string) *Qbs { 221 | q.criteria.orderBys = append(q.criteria.orderBys, order{q.Dialect.quote(path), false}) 222 | return q 223 | } 224 | 225 | func (q *Qbs) OrderByDesc(path string) *Qbs { 226 | q.criteria.orderBys = append(q.criteria.orderBys, order{q.Dialect.quote(path), true}) 227 | return q 228 | } 229 | 230 | // Camel case field names 231 | func (q *Qbs) OmitFields(fieldName ...string) *Qbs { 232 | q.criteria.omitFields = fieldName 233 | return q 234 | } 235 | 236 | func (q *Qbs) OmitJoin() *Qbs { 237 | q.criteria.omitJoin = true 238 | return q 239 | } 240 | 241 | // Perform select query by parsing the struct's type and then fill the values into the struct 242 | // All fields of supported types in the struct will be added in select clause. 243 | // If Id value is provided, it will be added into the where clause 244 | // If a foreign key field with its referenced struct pointer field are provided, 245 | // It will perform a join query, the referenced struct pointer field will be filled in 246 | // the values obtained by the query. 247 | // If not found, "sql.ErrNoRows" will be returned. 248 | func (q *Qbs) Find(structPtr interface{}) error { 249 | q.criteria.model = structPtrToModel(structPtr, !q.criteria.omitJoin, q.criteria.omitFields) 250 | q.criteria.limit = 1 251 | if !q.criteria.model.pkZero() { 252 | idPath := q.Dialect.quote(q.criteria.model.table) + "." + q.Dialect.quote(q.criteria.model.pk.name) 253 | idCondition := NewCondition(idPath+" = ?", q.criteria.model.pk.value) 254 | if q.criteria.condition == nil { 255 | q.criteria.condition = idCondition 256 | } else { 257 | q.criteria.condition = idCondition.AndCondition(q.criteria.condition) 258 | } 259 | } 260 | query, args := q.Dialect.querySql(q.criteria) 261 | return q.doQueryRow(structPtr, query, args...) 262 | } 263 | 264 | // Similar to Find, except that FindAll accept pointer of slice of struct pointer, 265 | // rows will be appended to the slice. 266 | func (q *Qbs) FindAll(ptrOfSliceOfStructPtr interface{}) error { 267 | strucType := reflect.TypeOf(ptrOfSliceOfStructPtr).Elem().Elem().Elem() 268 | strucPtr := reflect.New(strucType).Interface() 269 | q.criteria.model = structPtrToModel(strucPtr, !q.criteria.omitJoin, q.criteria.omitFields) 270 | query, args := q.Dialect.querySql(q.criteria) 271 | return q.doQueryRows(ptrOfSliceOfStructPtr, query, args...) 272 | } 273 | 274 | func (q *Qbs) doQueryRow(out interface{}, query string, args ...interface{}) error { 275 | defer q.Reset() 276 | rowValue := reflect.ValueOf(out) 277 | q.log(query, args...) 278 | stmt, err := q.prepare(query) 279 | if err != nil { 280 | return q.updateTxError(err) 281 | } 282 | rows, err := stmt.Query(args...) 283 | if err != nil { 284 | return q.updateTxError(err) 285 | } 286 | defer rows.Close() 287 | if rows.Next() { 288 | err = q.scanRows(rowValue, rows) 289 | if err != nil { 290 | return err 291 | } 292 | } else { 293 | return sql.ErrNoRows 294 | } 295 | return nil 296 | } 297 | 298 | func (q *Qbs) doQueryRows(out interface{}, query string, args ...interface{}) error { 299 | defer q.Reset() 300 | sliceValue := reflect.Indirect(reflect.ValueOf(out)) 301 | structType := sliceValue.Type().Elem().Elem() 302 | q.log(query, args...) 303 | stmt, err := q.prepare(query) 304 | if err != nil { 305 | return q.updateTxError(err) 306 | } 307 | rows, err := stmt.Query(args...) 308 | if err != nil { 309 | return q.updateTxError(err) 310 | } 311 | defer rows.Close() 312 | for rows.Next() { 313 | rowValue := reflect.New(structType) 314 | err = q.scanRows(rowValue, rows) 315 | if err != nil { 316 | return err 317 | } 318 | sliceValue.Set(reflect.Append(sliceValue, rowValue)) 319 | } 320 | return nil 321 | } 322 | 323 | func (q *Qbs) scanRows(rowValue reflect.Value, rows *sql.Rows) (err error) { 324 | cols, _ := rows.Columns() 325 | containers := make([]interface{}, 0, len(cols)) 326 | for i := 0; i < cap(containers); i++ { 327 | var v interface{} 328 | containers = append(containers, &v) 329 | } 330 | err = rows.Scan(containers...) 331 | if err != nil { 332 | return 333 | } 334 | for i, v := range containers { 335 | value := reflect.Indirect(reflect.ValueOf(v)) 336 | if !value.Elem().IsValid() { 337 | continue 338 | } 339 | key := cols[i] 340 | paths := strings.Split(key, "___") 341 | if len(paths) == 2 { 342 | subStruct := rowValue.Elem().FieldByName(TableNameToStructName(paths[0])) 343 | if subStruct.IsNil() { 344 | subStruct.Set(reflect.New(subStruct.Type().Elem())) 345 | } 346 | subField := subStruct.Elem().FieldByName(ColumnNameToFieldName(paths[1])) 347 | if subField.IsValid() { 348 | err = q.Dialect.setModelValue(value, subField) 349 | if err != nil { 350 | return 351 | } 352 | } 353 | } else { 354 | field := rowValue.Elem().FieldByName(ColumnNameToFieldName(key)) 355 | if field.IsValid() { 356 | err = q.Dialect.setModelValue(value, field) 357 | if err != nil { 358 | return 359 | } 360 | } 361 | } 362 | } 363 | return 364 | } 365 | 366 | // Same as sql.Db.Exec or sql.Tx.Exec depends on if transaction has began 367 | func (q *Qbs) Exec(query string, args ...interface{}) (sql.Result, error) { 368 | defer q.Reset() 369 | query = q.Dialect.substituteMarkers(query) 370 | q.log(query, args...) 371 | stmt, err := q.prepare(query) 372 | if err != nil { 373 | return nil, q.updateTxError(err) 374 | } 375 | result, err := stmt.Exec(args...) 376 | if err != nil { 377 | return nil, q.updateTxError(err) 378 | } 379 | return result, nil 380 | } 381 | 382 | // Same as sql.Db.QueryRow or sql.Tx.QueryRow depends on if transaction has began 383 | func (q *Qbs) QueryRow(query string, args ...interface{}) *sql.Row { 384 | q.log(query, args...) 385 | query = q.Dialect.substituteMarkers(query) 386 | stmt, err := q.prepare(query) 387 | if err != nil { 388 | q.updateTxError(err) 389 | return nil 390 | } 391 | return stmt.QueryRow(args...) 392 | } 393 | 394 | // Same as sql.Db.Query or sql.Tx.Query depends on if transaction has began 395 | func (q *Qbs) Query(query string, args ...interface{}) (rows *sql.Rows, err error) { 396 | q.log(query, args...) 397 | query = q.Dialect.substituteMarkers(query) 398 | stmt, err := q.prepare(query) 399 | if err != nil { 400 | q.updateTxError(err) 401 | return 402 | } 403 | return stmt.Query(args...) 404 | } 405 | 406 | // Same as sql.Db.Prepare or sql.Tx.Prepare depends on if transaction has began 407 | func (q *Qbs) prepare(query string) (stmt *sql.Stmt, err error) { 408 | var ok bool 409 | if q.tx != nil { 410 | stmt, ok = q.txStmtMap[query] 411 | if ok { 412 | return 413 | } 414 | stmt, err = q.tx.Prepare(query) 415 | if err != nil { 416 | q.updateTxError(err) 417 | return 418 | } 419 | q.txStmtMap[query] = stmt 420 | } else { 421 | mu.RLock() 422 | stmt, ok = stmtMap[query] 423 | mu.RUnlock() 424 | if ok { 425 | return 426 | } 427 | 428 | stmt, err = db.Prepare(query + ";") 429 | if err != nil { 430 | q.updateTxError(err) 431 | return 432 | } 433 | mu.Lock() 434 | stmtMap[query] = stmt 435 | mu.Unlock() 436 | } 437 | return 438 | } 439 | 440 | // If Id value is not provided, save will insert the record, and the Id value will 441 | // be filled in the struct after insertion. 442 | // If Id value is provided, save will do a query count first to see if the row exists, if not then insert it, 443 | // otherwise update it. 444 | // If struct implements Validator interface, it will be validated first 445 | func (q *Qbs) Save(structPtr interface{}) (affected int64, err error) { 446 | if v, ok := structPtr.(Validator); ok { 447 | err = v.Validate(q) 448 | if err != nil { 449 | return 450 | } 451 | } 452 | model := structPtrToModel(structPtr, true, q.criteria.omitFields) 453 | if model.pk == nil { 454 | panic("no primary key field") 455 | } 456 | q.criteria.model = model 457 | now := time.Now() 458 | var id int64 = 0 459 | updateModelField := model.timeField("updated") 460 | if updateModelField != nil { 461 | updateModelField.value = now 462 | } 463 | createdModelField := model.timeField("created") 464 | var isInsert bool 465 | if !model.pkZero() && q.WhereEqual(model.pk.name, model.pk.value).Count(model.table) > 0 { //id is given, can be an update operation. 466 | affected, err = q.Dialect.update(q) 467 | } else { 468 | if createdModelField != nil { 469 | createdModelField.value = now 470 | } 471 | id, err = q.Dialect.insert(q) 472 | isInsert = true 473 | if err == nil { 474 | affected = 1 475 | } 476 | } 477 | if err == nil { 478 | structValue := reflect.Indirect(reflect.ValueOf(structPtr)) 479 | if _, ok := model.pk.value.(int64); ok && id != 0 { 480 | idField := structValue.FieldByName(model.pk.camelName) 481 | idField.SetInt(id) 482 | } 483 | if updateModelField != nil { 484 | updateField := structValue.FieldByName(updateModelField.camelName) 485 | updateField.Set(reflect.ValueOf(now)) 486 | } 487 | if isInsert { 488 | if createdModelField != nil { 489 | createdField := structValue.FieldByName(createdModelField.camelName) 490 | createdField.Set(reflect.ValueOf(now)) 491 | } 492 | } 493 | } 494 | return affected, q.updateTxError(err) 495 | } 496 | 497 | func (q *Qbs) BulkInsert(sliceOfStructPtr interface{}) error { 498 | defer q.Reset() 499 | var err error 500 | if q.tx == nil { 501 | q.Begin() 502 | defer func() { 503 | if err != nil { 504 | q.Rollback() 505 | } else { 506 | q.Commit() 507 | } 508 | }() 509 | } 510 | sliceValue := reflect.ValueOf(sliceOfStructPtr) 511 | for i := 0; i < sliceValue.Len(); i++ { 512 | structPtr := sliceValue.Index(i) 513 | structPtrInter := structPtr.Interface() 514 | if v, ok := structPtrInter.(Validator); ok { 515 | err = v.Validate(q) 516 | if err != nil { 517 | return q.updateTxError(err) 518 | } 519 | } 520 | model := structPtrToModel(structPtrInter, false, nil) 521 | if model.pk == nil { 522 | panic("no primary key field") 523 | } 524 | q.criteria.model = model 525 | var id int64 526 | id, err = q.Dialect.insert(q) 527 | if err != nil { 528 | return q.updateTxError(err) 529 | } 530 | if _, ok := model.pk.value.(int64); ok && id != 0 { 531 | idField := structPtr.Elem().FieldByName(model.pk.camelName) 532 | idField.SetInt(id) 533 | } 534 | } 535 | return nil 536 | } 537 | 538 | // If the struct type implements Validator interface, values will be validated before update. 539 | // In order to avoid inadvertently update the struct field to zero value, it is better to define a 540 | // temporary struct in function, only define the fields that should be updated. 541 | // But the temporary struct can not implement Validator interface, we have to validate values manually. 542 | // The update condition can be inferred by the Id value of the struct. 543 | // If neither Id value or condition are provided, it would cause runtime panic 544 | func (q *Qbs) Update(structPtr interface{}) (affected int64, err error) { 545 | if v, ok := structPtr.(Validator); ok { 546 | err := v.Validate(q) 547 | if err != nil { 548 | return 0, err 549 | } 550 | } 551 | model := structPtrToModel(structPtr, true, q.criteria.omitFields) 552 | q.criteria.model = model 553 | q.criteria.mergePkCondition(q.Dialect) 554 | if q.criteria.condition == nil { 555 | panic("Can not update without condition") 556 | } 557 | return q.Dialect.update(q) 558 | } 559 | 560 | // The delete condition can be inferred by the Id value of the struct 561 | // If neither Id value or condition are provided, it would cause runtime panic 562 | func (q *Qbs) Delete(structPtr interface{}) (affected int64, err error) { 563 | model := structPtrToModel(structPtr, true, q.criteria.omitFields) 564 | q.criteria.model = model 565 | q.criteria.mergePkCondition(q.Dialect) 566 | if q.criteria.condition == nil { 567 | panic("Can not delete without condition") 568 | } 569 | return q.Dialect.delete(q) 570 | } 571 | 572 | // This method can be used to validate unique column before trying to save 573 | // The table parameter can be either a string or a struct pointer 574 | func (q *Qbs) ContainsValue(table interface{}, column string, value interface{}) bool { 575 | quotedColumn := q.Dialect.quote(column) 576 | quotedTable := q.Dialect.quote(tableName(table)) 577 | query := fmt.Sprintf("SELECT %v FROM %v WHERE %v = ?", quotedColumn, quotedTable, quotedColumn) 578 | row := q.QueryRow(query, value) 579 | var result interface{} 580 | err := row.Scan(&result) 581 | q.updateTxError(err) 582 | return err == nil 583 | } 584 | 585 | // If the connection pool is not full, the Db will be sent back into the pool, otherwise the Db will get closed. 586 | func (q *Qbs) Close() error { 587 | if connectionLimit != nil { 588 | <-connectionLimit 589 | } 590 | if q.tx != nil { 591 | return q.Rollback() 592 | } 593 | return nil 594 | } 595 | 596 | //Query the count of rows in a table the talbe parameter can be either a string or struct pointer. 597 | //If condition is given, the count will be the count of rows meet that condition. 598 | func (q *Qbs) Count(table interface{}) int64 { 599 | quotedTable := q.Dialect.quote(tableName(table)) 600 | query := "SELECT COUNT(*) FROM " + quotedTable 601 | var row *sql.Row 602 | if q.criteria.condition != nil { 603 | conditionSql, args := q.criteria.condition.Merge() 604 | query += " WHERE " + conditionSql 605 | row = q.QueryRow(query, args...) 606 | } else { 607 | row = q.QueryRow(query) 608 | } 609 | var count int64 610 | err := row.Scan(&count) 611 | if err == sql.ErrNoRows { 612 | return 0 613 | } else if err != nil { 614 | q.updateTxError(err) 615 | } 616 | return count 617 | } 618 | 619 | //Query raw sql and return a map. 620 | func (q *Qbs) QueryMap(query string, args ...interface{}) (map[string]interface{}, error) { 621 | mapSlice, err := q.doQueryMap(query, true, args...) 622 | if len(mapSlice) == 1 { 623 | return mapSlice[0], err 624 | } 625 | return nil, sql.ErrNoRows 626 | 627 | } 628 | 629 | //Query raw sql and return a slice of map.. 630 | func (q *Qbs) QueryMapSlice(query string, args ...interface{}) ([]map[string]interface{}, error) { 631 | return q.doQueryMap(query, false, args...) 632 | } 633 | 634 | func (q *Qbs) doQueryMap(query string, once bool, args ...interface{}) ([]map[string]interface{}, error) { 635 | query = q.Dialect.substituteMarkers(query) 636 | stmt, err := q.prepare(query) 637 | if err != nil { 638 | return nil, q.updateTxError(err) 639 | } 640 | rows, err := stmt.Query(args...) 641 | if err != nil { 642 | return nil, q.updateTxError(err) 643 | } 644 | defer rows.Close() 645 | var results []map[string]interface{} 646 | columns, _ := rows.Columns() 647 | containers := make([]interface{}, len(columns)) 648 | for i := 0; i < len(columns); i++ { 649 | var container interface{} 650 | containers[i] = &container 651 | } 652 | for rows.Next() { 653 | if err := rows.Scan(containers...); err != nil { 654 | return nil, q.updateTxError(err) 655 | } 656 | result := make(map[string]interface{}, len(columns)) 657 | for i, key := range columns { 658 | if containers[i] == nil { 659 | continue 660 | } 661 | value := reflect.Indirect(reflect.ValueOf(containers[i])) 662 | if value.Elem().Kind() == reflect.Slice { 663 | result[key] = string(value.Interface().([]byte)) 664 | } else { 665 | result[key] = value.Interface() 666 | } 667 | } 668 | results = append(results, result) 669 | if once { 670 | return results, nil 671 | } 672 | } 673 | return results, nil 674 | } 675 | 676 | //Do a raw sql query and set the result values in dest parameter. 677 | //The dest parameter can be either a struct pointer or a pointer of struct pointer.slice 678 | //This method do not support pointer field in the struct. 679 | func (q *Qbs) QueryStruct(dest interface{}, query string, args ...interface{}) error { 680 | query = q.Dialect.substituteMarkers(query) 681 | stmt, err := q.prepare(query) 682 | if err != nil { 683 | return q.updateTxError(err) 684 | } 685 | rows, err := stmt.Query(args...) 686 | if err != nil { 687 | return q.updateTxError(err) 688 | } 689 | defer rows.Close() 690 | outPtr := reflect.ValueOf(dest) 691 | outValue := outPtr.Elem() 692 | var structType reflect.Type 693 | var single bool 694 | if outValue.Kind() == reflect.Slice { 695 | structType = outValue.Type().Elem().Elem() 696 | } else { 697 | structType = outValue.Type() 698 | single = true 699 | } 700 | columns, _ := rows.Columns() 701 | fieldNames := make([]string, len(columns)) 702 | for i, v := range columns { 703 | upper := snakeToUpperCamel(v) 704 | _, ok := structType.FieldByName(upper) 705 | if ok { 706 | fieldNames[i] = upper 707 | } else { 708 | fieldNames[i] = "-" 709 | } 710 | } 711 | for rows.Next() { 712 | var rowStructPointer reflect.Value 713 | if single { //query row 714 | rowStructPointer = outPtr 715 | } else { //query rows 716 | rowStructPointer = reflect.New(structType) 717 | } 718 | dests := make([]interface{}, len(columns)) 719 | for i := 0; i < len(dests); i++ { 720 | fieldName := fieldNames[i] 721 | if fieldName == "-" { 722 | var placeholder interface{} 723 | dests[i] = &placeholder 724 | } else { 725 | field := rowStructPointer.Elem().FieldByName(fieldName) 726 | dests[i] = field.Addr().Interface() 727 | } 728 | } 729 | err = rows.Scan(dests...) 730 | if err != nil { 731 | return err 732 | } 733 | if single { 734 | return nil 735 | } 736 | outValue.Set(reflect.Append(outValue, rowStructPointer)) 737 | } 738 | return nil 739 | } 740 | 741 | //Iterate the rows, the first parameter is a struct pointer, the second parameter is a fucntion 742 | //which will get called on each row, the in `do` function the structPtr's value will be set to the current row's value.. 743 | //if `do` function returns an error, the iteration will be stopped. 744 | func (q *Qbs) Iterate(structPtr interface{}, do func() error) error { 745 | q.criteria.model = structPtrToModel(structPtr, !q.criteria.omitJoin, q.criteria.omitFields) 746 | query, args := q.Dialect.querySql(q.criteria) 747 | q.log(query, args...) 748 | defer q.Reset() 749 | stmt, err := q.prepare(query) 750 | if err != nil { 751 | return q.updateTxError(err) 752 | } 753 | rows, err := stmt.Query(args...) 754 | if err != nil { 755 | return q.updateTxError(err) 756 | } 757 | rowValue := reflect.ValueOf(structPtr) 758 | defer rows.Close() 759 | for rows.Next() { 760 | err = q.scanRows(rowValue, rows) 761 | if err != nil { 762 | return err 763 | } 764 | if err = do(); err != nil { 765 | return err 766 | } 767 | } 768 | return nil 769 | } 770 | 771 | func (q *Qbs) log(query string, args ...interface{}) { 772 | if q.Log && queryLogger != nil { 773 | queryLogger.Print(query) 774 | queryLogger.Println(args...) 775 | } 776 | } 777 | --------------------------------------------------------------------------------