From 97fe773491ae69531141316a1178d22c8a5d1257 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sat, 27 Jan 2018 09:20:59 -0600 Subject: [PATCH] fix MSSQL bug on org (#3405) --- .../go-xorm/builder/builder_select.go | 4 + vendor/github.com/go-xorm/builder/cond_and.go | 6 +- vendor/github.com/go-xorm/core/cache.go | 10 +- vendor/github.com/go-xorm/core/circle.yml | 3 +- vendor/github.com/go-xorm/core/column.go | 16 +- vendor/github.com/go-xorm/core/scan.go | 3 + .../github.com/go-xorm/xorm/CONTRIBUTING.md | 9 +- vendor/github.com/go-xorm/xorm/README.md | 155 ++++++++++++-- vendor/github.com/go-xorm/xorm/README_CN.md | 136 +++++++++++- vendor/github.com/go-xorm/xorm/context.go | 26 +++ vendor/github.com/go-xorm/xorm/convert.go | 6 +- .../go-xorm/xorm/dialect_postgres.go | 79 +++---- vendor/github.com/go-xorm/xorm/engine.go | 96 ++++++--- .../github.com/go-xorm/xorm/engine_group.go | 194 ++++++++++++++++++ .../go-xorm/xorm/engine_group_policy.go | 116 +++++++++++ .../github.com/go-xorm/xorm/engine_maxlife.go | 8 + vendor/github.com/go-xorm/xorm/error.go | 2 + vendor/github.com/go-xorm/xorm/interface.go | 103 ++++++++++ vendor/github.com/go-xorm/xorm/session.go | 9 +- .../go-xorm/xorm/session_convert.go | 14 +- .../github.com/go-xorm/xorm/session_exist.go | 13 +- vendor/github.com/go-xorm/xorm/session_get.go | 8 + .../github.com/go-xorm/xorm/session_insert.go | 4 +- .../github.com/go-xorm/xorm/session_query.go | 91 +++++++- vendor/github.com/go-xorm/xorm/session_raw.go | 13 +- .../github.com/go-xorm/xorm/session_update.go | 21 +- vendor/github.com/go-xorm/xorm/statement.go | 12 +- vendor/vendor.json | 18 +- 28 files changed, 1011 insertions(+), 164 deletions(-) create mode 100644 vendor/github.com/go-xorm/xorm/context.go create mode 100644 vendor/github.com/go-xorm/xorm/engine_group.go create mode 100644 vendor/github.com/go-xorm/xorm/engine_group_policy.go create mode 100644 vendor/github.com/go-xorm/xorm/interface.go diff --git a/vendor/github.com/go-xorm/builder/builder_select.go b/vendor/github.com/go-xorm/builder/builder_select.go index 05f116e005..3a3967cccc 100644 --- a/vendor/github.com/go-xorm/builder/builder_select.go +++ b/vendor/github.com/go-xorm/builder/builder_select.go @@ -45,6 +45,10 @@ func (b *Builder) selectWriteTo(w Writer) error { } } + if !b.cond.IsValid() { + return nil + } + if _, err := fmt.Fprint(w, " WHERE "); err != nil { return err } diff --git a/vendor/github.com/go-xorm/builder/cond_and.go b/vendor/github.com/go-xorm/builder/cond_and.go index 9c30e9c2e5..e30bd186cd 100644 --- a/vendor/github.com/go-xorm/builder/cond_and.go +++ b/vendor/github.com/go-xorm/builder/cond_and.go @@ -25,7 +25,9 @@ func And(conds ...Cond) Cond { func (and condAnd) WriteTo(w Writer) error { for i, cond := range and { _, isOr := cond.(condOr) - if isOr { + _, isExpr := cond.(expr) + wrap := isOr || isExpr + if wrap { fmt.Fprint(w, "(") } @@ -34,7 +36,7 @@ func (and condAnd) WriteTo(w Writer) error { return err } - if isOr { + if wrap { fmt.Fprint(w, ")") } diff --git a/vendor/github.com/go-xorm/core/cache.go b/vendor/github.com/go-xorm/core/cache.go index bf81bd52ba..8f9531da94 100644 --- a/vendor/github.com/go-xorm/core/cache.go +++ b/vendor/github.com/go-xorm/core/cache.go @@ -1,11 +1,12 @@ package core import ( - "errors" - "fmt" - "time" "bytes" "encoding/gob" + "errors" + "fmt" + "strings" + "time" ) const ( @@ -55,11 +56,10 @@ func encodeIds(ids []PK) (string, error) { return buf.String(), err } - func decodeIds(s string) ([]PK, error) { pks := make([]PK, 0) - dec := gob.NewDecoder(bytes.NewBufferString(s)) + dec := gob.NewDecoder(strings.NewReader(s)) err := dec.Decode(&pks) return pks, err diff --git a/vendor/github.com/go-xorm/core/circle.yml b/vendor/github.com/go-xorm/core/circle.yml index 006e1230bc..e6a05be272 100644 --- a/vendor/github.com/go-xorm/core/circle.yml +++ b/vendor/github.com/go-xorm/core/circle.yml @@ -11,4 +11,5 @@ database: test: override: # './...' is a relative pattern which means all subdirectories - - go test -v -race \ No newline at end of file + - go test -v -race + - go test -v -race --dbtype=sqlite3 diff --git a/vendor/github.com/go-xorm/core/column.go b/vendor/github.com/go-xorm/core/column.go index d9362e9857..65370bb5ba 100644 --- a/vendor/github.com/go-xorm/core/column.go +++ b/vendor/github.com/go-xorm/core/column.go @@ -79,6 +79,10 @@ func (col *Column) String(d Dialect) string { } } + if col.Default != "" { + sql += "DEFAULT " + col.Default + " " + } + if d.ShowCreateNull() { if col.Nullable { sql += "NULL " @@ -87,10 +91,6 @@ func (col *Column) String(d Dialect) string { } } - if col.Default != "" { - sql += "DEFAULT " + col.Default + " " - } - return sql } @@ -99,6 +99,10 @@ func (col *Column) StringNoPk(d Dialect) string { sql += d.SqlType(col) + " " + if col.Default != "" { + sql += "DEFAULT " + col.Default + " " + } + if d.ShowCreateNull() { if col.Nullable { sql += "NULL " @@ -107,10 +111,6 @@ func (col *Column) StringNoPk(d Dialect) string { } } - if col.Default != "" { - sql += "DEFAULT " + col.Default + " " - } - return sql } diff --git a/vendor/github.com/go-xorm/core/scan.go b/vendor/github.com/go-xorm/core/scan.go index 7da338d864..b7c159b274 100644 --- a/vendor/github.com/go-xorm/core/scan.go +++ b/vendor/github.com/go-xorm/core/scan.go @@ -44,6 +44,9 @@ func convertTime(dest *NullTime, src interface{}) error { } *dest = NullTime(t) return nil + case time.Time: + *dest = NullTime(s) + return nil case nil: default: return fmt.Errorf("unsupported driver -> Scan pair: %T -> %T", src, dest) diff --git a/vendor/github.com/go-xorm/xorm/CONTRIBUTING.md b/vendor/github.com/go-xorm/xorm/CONTRIBUTING.md index e0f6cfcdf2..37f4bc5fa8 100644 --- a/vendor/github.com/go-xorm/xorm/CONTRIBUTING.md +++ b/vendor/github.com/go-xorm/xorm/CONTRIBUTING.md @@ -32,13 +32,10 @@ proposed functionality. We appreciate any bug reports, but especially ones with self-contained (doesn't depend on code outside of xorm), minimal (can't be simplified further) test cases. It's especially helpful if you can submit a pull -request with just the failing test case (you'll probably want to -pattern it after the tests in -[base.go](https://github.com/go-xorm/tests/blob/master/base.go) AND -[benchmark.go](https://github.com/go-xorm/tests/blob/master/benchmark.go). +request with just the failing test case(you can find some example test file like [session_get_test.go](https://github.com/go-xorm/xorm/blob/master/session_get_test.go)). -If you implements a new database interface, you maybe need to add a _test.go file. -For example, [mysql_test.go](https://github.com/go-xorm/tests/blob/master/mysql/mysql_test.go) +If you implements a new database interface, you maybe need to add a test_.sh file. +For example, [mysql_test.go](https://github.com/go-xorm/xorm/blob/master/test_mysql.sh) ### New functionality diff --git a/vendor/github.com/go-xorm/xorm/README.md b/vendor/github.com/go-xorm/xorm/README.md index c8c43894c3..9f6c20cbda 100644 --- a/vendor/github.com/go-xorm/xorm/README.md +++ b/vendor/github.com/go-xorm/xorm/README.md @@ -28,6 +28,8 @@ Xorm is a simple and powerful ORM for Go. * SQL Builder support via [github.com/go-xorm/builder](https://github.com/go-xorm/builder) +* Automatical Read/Write seperatelly + # Drivers Support Drivers for Go's sql package which currently support database/sql includes: @@ -48,6 +50,13 @@ Drivers for Go's sql package which currently support database/sql includes: # Changelog +* **v0.6.4** + * Automatical Read/Write seperatelly + * Query/QueryString/QueryInterface and action with Where/And + * Get support non-struct variables + * BufferSize on Iterate + * fix some other bugs. + * **v0.6.3** * merge tests to main project * add `Exist` function @@ -61,13 +70,6 @@ Drivers for Go's sql package which currently support database/sql includes: * add Scan features to Get * add QueryString method -* **v0.6.0** - * remove support for ql - * add query condition builder support via [github.com/go-xorm/builder](https://github.com/go-xorm/builder), so `Where`, `And`, `Or` -methods can use `builder.Cond` as parameter - * add Sum, SumInt, SumInt64 and NotIn methods - * some bugs fixed - [More changes ...](https://github.com/go-xorm/manual-en-US/tree/master/chapter-16) # Installation @@ -106,15 +108,36 @@ type User struct { err := engine.Sync2(new(User)) ``` -* `Query` runs a SQL string, the returned results is `[]map[string][]byte`, `QueryString` returns `[]map[string]string`. +* Create Engine Group + +```Go +dataSourceNameSlice := []string{masterDataSourceName, slave1DataSourceName, slave2DataSourceName} +engineGroup, err := xorm.NewEngineGroup(driverName, dataSourceNameSlice) +``` + +```Go +masterEngine, err := xorm.NewEngine(driverName, masterDataSourceName) +slave1Engine, err := xorm.NewEngine(driverName, slave1DataSourceName) +slave2Engine, err := xorm.NewEngine(driverName, slave2DataSourceName) +engineGroup, err := xorm.NewEngineGroup(masterEngine, []*Engine{slave1Engine, slave2Engine}) +``` + +Then all place where `engine` you can just use `engineGroup`. + +* `Query` runs a SQL string, the returned results is `[]map[string][]byte`, `QueryString` returns `[]map[string]string`, `QueryInterface` returns `[]map[string]interface{}`. ```Go results, err := engine.Query("select * from user") +results, err := engine.Where("a = 1").Query() results, err := engine.QueryString("select * from user") +results, err := engine.Where("a = 1").QueryString() + +results, err := engine.QueryInterface("select * from user") +results, err := engine.Where("a = 1").QueryInterface() ``` -* `Execute` runs a SQL string, it returns `affected` and `error` +* `Exec` runs a SQL string, it returns `affected` and `error` ```Go affected, err := engine.Exec("update user set age = ? where name = ?", age, name) @@ -125,62 +148,76 @@ affected, err := engine.Exec("update user set age = ? where name = ?", age, name ```Go affected, err := engine.Insert(&user) // INSERT INTO struct () values () + affected, err := engine.Insert(&user1, &user2) // INSERT INTO struct1 () values () // INSERT INTO struct2 () values () + affected, err := engine.Insert(&users) // INSERT INTO struct () values (),(),() + affected, err := engine.Insert(&user1, &users) // INSERT INTO struct1 () values () // INSERT INTO struct2 () values (),(),() ``` -* Query one record from database +* `Get` query one record from database ```Go has, err := engine.Get(&user) // SELECT * FROM user LIMIT 1 + has, err := engine.Where("name = ?", name).Desc("id").Get(&user) // SELECT * FROM user WHERE name = ? ORDER BY id DESC LIMIT 1 + var name string has, err := engine.Where("id = ?", id).Cols("name").Get(&name) // SELECT name FROM user WHERE id = ? + var id int64 has, err := engine.Where("name = ?", name).Cols("id").Get(&id) +has, err := engine.SQL("select id from user").Get(&id) // SELECT id FROM user WHERE name = ? + var valuesMap = make(map[string]string) has, err := engine.Where("id = ?", id).Get(&valuesMap) // SELECT * FROM user WHERE id = ? + var valuesSlice = make([]interface{}, len(cols)) has, err := engine.Where("id = ?", id).Cols(cols...).Get(&valuesSlice) // SELECT col1, col2, col3 FROM user WHERE id = ? ``` -* Check if one record exist on table +* `Exist` check if one record exist on table ```Go has, err := testEngine.Exist(new(RecordExist)) // SELECT * FROM record_exist LIMIT 1 + has, err = testEngine.Exist(&RecordExist{ Name: "test1", }) // SELECT * FROM record_exist WHERE name = ? LIMIT 1 + has, err = testEngine.Where("name = ?", "test1").Exist(&RecordExist{}) // SELECT * FROM record_exist WHERE name = ? LIMIT 1 + has, err = testEngine.SQL("select * from record_exist where name = ?", "test1").Exist() // select * from record_exist where name = ? + has, err = testEngine.Table("record_exist").Exist() // SELECT * FROM record_exist LIMIT 1 + has, err = testEngine.Table("record_exist").Where("name = ?", "test1").Exist() // SELECT * FROM record_exist WHERE name = ? LIMIT 1 ``` -* Query multiple records from database, also you can use join and extends +* `Find` query multiple records from database, also you can use join and extends ```Go var users []User err := engine.Where("name = ?", name).And("age > 10").Limit(10, 0).Find(&users) -// SELECT * FROM user WHERE name = ? AND age > 10 limit 0 offset 10 +// SELECT * FROM user WHERE name = ? AND age > 10 limit 10 offset 0 type Detail struct { Id int64 @@ -193,14 +230,14 @@ type UserDetail struct { } var users []UserDetail -err := engine.Table("user").Select("user.*, detail.*") +err := engine.Table("user").Select("user.*, detail.*"). Join("INNER", "detail", "detail.user_id = user.id"). Where("user.name = ?", name).Limit(10, 0). Find(&users) -// SELECT user.*, detail.* FROM user INNER JOIN detail WHERE user.name = ? limit 0 offset 10 +// SELECT user.*, detail.* FROM user INNER JOIN detail WHERE user.name = ? limit 10 offset 0 ``` -* Query multiple records and record by record handle, there are two methods Iterate and Rows +* `Iterate` and `Rows` query multiple records and record by record handle, there are two methods Iterate and Rows ```Go err := engine.Iterate(&User{Name:name}, func(idx int, bean interface{}) error { @@ -209,6 +246,13 @@ err := engine.Iterate(&User{Name:name}, func(idx int, bean interface{}) error { }) // SELECT * FROM user +err := engine.BufferSize(100).Iterate(&User{Name:name}, func(idx int, bean interface{}) error { + user := bean.(*User) + return nil +}) +// SELECT * FROM user Limit 0, 100 +// SELECT * FROM user Limit 101, 100 + rows, err := engine.Rows(&User{Name:name}) // SELECT * FROM user defer rows.Close() @@ -218,7 +262,7 @@ for rows.Next() { } ``` -* Update one or more records, default will update non-empty and non-zero fields except when you use Cols, AllCols and so on. +* `Update` update one or more records, default will update non-empty and non-zero fields except when you use Cols, AllCols and so on. ```Go affected, err := engine.Id(1).Update(&user) @@ -243,21 +287,39 @@ affected, err := engine.Id(1).AllCols().Update(&user) // UPDATE user SET name=?,age=?,salt=?,passwd=?,updated=? Where id = ? ``` -* Delete one or more records, Delete MUST have condition +* `Delete` delete one or more records, Delete MUST have condition ```Go affected, err := engine.Where(...).Delete(&user) // DELETE FROM user Where ... -affected, err := engine.Id(2).Delete(&user) + +affected, err := engine.ID(2).Delete(&user) +// DELETE FROM user Where id = ? ``` -* Count records +* `Count` count records ```Go counts, err := engine.Count(&user) // SELECT count(*) AS total FROM user ``` +* `Sum` sum functions + +```Go +agesFloat64, err := engine.Sum(&user, "age") +// SELECT sum(age) AS total FROM user + +agesInt64, err := engine.SumInt(&user, "age") +// SELECT sum(age) AS total FROM user + +sumFloat64Slice, err := engine.Sums(&user, "age", "score") +// SELECT sum(age), sum(score) FROM user + +sumInt64Slice, err := engine.SumsInt(&user, "age", "score") +// SELECT sum(age), sum(score) FROM user +``` + * Query conditions builder ```Go @@ -265,6 +327,59 @@ err := engine.Where(builder.NotIn("a", 1, 2).And(builder.In("b", "c", "d", "e")) // SELECT id, name ... FROM user WHERE a NOT IN (?, ?) AND b IN (?, ?, ?) ``` +* Multiple operations in one go routine, no transation here but resue session memory + +```Go +session := engine.NewSession() +defer session.Close() + +user1 := Userinfo{Username: "xiaoxiao", Departname: "dev", Alias: "lunny", Created: time.Now()} +if _, err := session.Insert(&user1); err != nil { + return err +} + +user2 := Userinfo{Username: "yyy"} +if _, err := session.Where("id = ?", 2).Update(&user2); err != nil { + return err +} + +if _, err := session.Exec("delete from userinfo where username = ?", user2.Username); err != nil { + return err +} + +return nil +``` + +* Transation should on one go routine. There is transaction and resue session memory + +```Go +session := engine.NewSession() +defer session.Close() + +// add Begin() before any action +if err := session.Begin(); err != nil { + // if returned then will rollback automatically + return err +} + +user1 := Userinfo{Username: "xiaoxiao", Departname: "dev", Alias: "lunny", Created: time.Now()} +if _, err := session.Insert(&user1); err != nil { + return err +} + +user2 := Userinfo{Username: "yyy"} +if _, err := session.Where("id = ?", 2).Update(&user2); err != nil { + return err +} + +if _, err := session.Exec("delete from userinfo where username = ?", user2.Username); err != nil { + return err +} + +// add Commit() after all actions +return session.Commit() +``` + # Cases * [studygolang](http://studygolang.com/) - [github.com/studygolang/studygolang](https://github.com/studygolang/studygolang) diff --git a/vendor/github.com/go-xorm/xorm/README_CN.md b/vendor/github.com/go-xorm/xorm/README_CN.md index cb2c1799ea..b4258d5405 100644 --- a/vendor/github.com/go-xorm/xorm/README_CN.md +++ b/vendor/github.com/go-xorm/xorm/README_CN.md @@ -115,12 +115,33 @@ type User struct { err := engine.Sync2(new(User)) ``` -* `Query` 最原始的也支持SQL语句查询,返回的结果类型为 []map[string][]byte。`QueryString` 返回 []map[string]string +* 创建Engine组 + +```Go +dataSourceNameSlice := []string{masterDataSourceName, slave1DataSourceName, slave2DataSourceName} +engineGroup, err := xorm.NewEngineGroup(driverName, dataSourceNameSlice) +``` + +```Go +masterEngine, err := xorm.NewEngine(driverName, masterDataSourceName) +slave1Engine, err := xorm.NewEngine(driverName, slave1DataSourceName) +slave2Engine, err := xorm.NewEngine(driverName, slave2DataSourceName) +engineGroup, err := xorm.NewEngineGroup(masterEngine, []*Engine{slave1Engine, slave2Engine}) +``` + +所有使用 `engine` 都可以简单的用 `engineGroup` 来替换。 + +* `Query` 最原始的也支持SQL语句查询,返回的结果类型为 []map[string][]byte。`QueryString` 返回 []map[string]string, `QueryInterface` 返回 `[]map[string]interface{}`. ```Go results, err := engine.Query("select * from user") +results, err := engine.Where("a = 1").Query() results, err := engine.QueryString("select * from user") +results, err := engine.Where("a = 1").QueryString() + +results, err := engine.QueryInterface("select * from user") +results, err := engine.Where("a = 1").QueryInterface() ``` * `Exec` 执行一个SQL语句 @@ -129,67 +150,81 @@ results, err := engine.QueryString("select * from user") affected, err := engine.Exec("update user set age = ? where name = ?", age, name) ``` -* 插入一条或者多条记录 +* `Insert` 插入一条或者多条记录 ```Go affected, err := engine.Insert(&user) // INSERT INTO struct () values () + affected, err := engine.Insert(&user1, &user2) // INSERT INTO struct1 () values () // INSERT INTO struct2 () values () + affected, err := engine.Insert(&users) // INSERT INTO struct () values (),(),() + affected, err := engine.Insert(&user1, &users) // INSERT INTO struct1 () values () // INSERT INTO struct2 () values (),(),() ``` -* 查询单条记录 +* `Get` 查询单条记录 ```Go has, err := engine.Get(&user) // SELECT * FROM user LIMIT 1 + has, err := engine.Where("name = ?", name).Desc("id").Get(&user) // SELECT * FROM user WHERE name = ? ORDER BY id DESC LIMIT 1 + var name string has, err := engine.Where("id = ?", id).Cols("name").Get(&name) // SELECT name FROM user WHERE id = ? + var id int64 has, err := engine.Where("name = ?", name).Cols("id").Get(&id) +has, err := engine.SQL("select id from user").Get(&id) // SELECT id FROM user WHERE name = ? + var valuesMap = make(map[string]string) has, err := engine.Where("id = ?", id).Get(&valuesMap) // SELECT * FROM user WHERE id = ? + var valuesSlice = make([]interface{}, len(cols)) has, err := engine.Where("id = ?", id).Cols(cols...).Get(&valuesSlice) // SELECT col1, col2, col3 FROM user WHERE id = ? ``` -* 检测记录是否存在 +* `Exist` 检测记录是否存在 ```Go has, err := testEngine.Exist(new(RecordExist)) // SELECT * FROM record_exist LIMIT 1 + has, err = testEngine.Exist(&RecordExist{ Name: "test1", }) // SELECT * FROM record_exist WHERE name = ? LIMIT 1 + has, err = testEngine.Where("name = ?", "test1").Exist(&RecordExist{}) // SELECT * FROM record_exist WHERE name = ? LIMIT 1 + has, err = testEngine.SQL("select * from record_exist where name = ?", "test1").Exist() // select * from record_exist where name = ? + has, err = testEngine.Table("record_exist").Exist() // SELECT * FROM record_exist LIMIT 1 + has, err = testEngine.Table("record_exist").Where("name = ?", "test1").Exist() // SELECT * FROM record_exist WHERE name = ? LIMIT 1 ``` -* 查询多条记录,当然可以使用Join和extends来组合使用 +* `Find` 查询多条记录,当然可以使用Join和extends来组合使用 ```Go var users []User err := engine.Where("name = ?", name).And("age > 10").Limit(10, 0).Find(&users) -// SELECT * FROM user WHERE name = ? AND age > 10 limit 0 offset 10 +// SELECT * FROM user WHERE name = ? AND age > 10 limit 10 offset 0 type Detail struct { Id int64 @@ -206,10 +241,10 @@ err := engine.Table("user").Select("user.*, detail.*") Join("INNER", "detail", "detail.user_id = user.id"). Where("user.name = ?", name).Limit(10, 0). Find(&users) -// SELECT user.*, detail.* FROM user INNER JOIN detail WHERE user.name = ? limit 0 offset 10 +// SELECT user.*, detail.* FROM user INNER JOIN detail WHERE user.name = ? limit 10 offset 0 ``` -* 根据条件遍历数据库,可以有两种方式: Iterate and Rows +* `Iterate` 和 `Rows` 根据条件遍历数据库,可以有两种方式: Iterate and Rows ```Go err := engine.Iterate(&User{Name:name}, func(idx int, bean interface{}) error { @@ -218,6 +253,13 @@ err := engine.Iterate(&User{Name:name}, func(idx int, bean interface{}) error { }) // SELECT * FROM user +err := engine.BufferSize(100).Iterate(&User{Name:name}, func(idx int, bean interface{}) error { + user := bean.(*User) + return nil +}) +// SELECT * FROM user Limit 0, 100 +// SELECT * FROM user Limit 101, 100 + rows, err := engine.Rows(&User{Name:name}) // SELECT * FROM user defer rows.Close() @@ -227,7 +269,7 @@ for rows.Next() { } ``` -* 更新数据,除非使用Cols,AllCols函数指明,默认只更新非空和非0的字段 +* `Update` 更新数据,除非使用Cols,AllCols函数指明,默认只更新非空和非0的字段 ```Go affected, err := engine.Id(1).Update(&user) @@ -252,20 +294,39 @@ affected, err := engine.Id(1).AllCols().Update(&user) // UPDATE user SET name=?,age=?,salt=?,passwd=?,updated=? Where id = ? ``` -* 删除记录,需要注意,删除必须至少有一个条件,否则会报错。要清空数据库可以用EmptyTable +* `Delete` 删除记录,需要注意,删除必须至少有一个条件,否则会报错。要清空数据库可以用EmptyTable ```Go affected, err := engine.Where(...).Delete(&user) // DELETE FROM user Where ... + +affected, err := engine.ID(2).Delete(&user) +// DELETE FROM user Where id = ? ``` -* 获取记录条数 +* `Count` 获取记录条数 ```Go counts, err := engine.Count(&user) // SELECT count(*) AS total FROM user ``` +* `Sum` 求和函数 + +```Go +agesFloat64, err := engine.Sum(&user, "age") +// SELECT sum(age) AS total FROM user + +agesInt64, err := engine.SumInt(&user, "age") +// SELECT sum(age) AS total FROM user + +sumFloat64Slice, err := engine.Sums(&user, "age", "score") +// SELECT sum(age), sum(score) FROM user + +sumInt64Slice, err := engine.SumsInt(&user, "age", "score") +// SELECT sum(age), sum(score) FROM user +``` + * 条件编辑器 ```Go @@ -273,6 +334,59 @@ err := engine.Where(builder.NotIn("a", 1, 2).And(builder.In("b", "c", "d", "e")) // SELECT id, name ... FROM user WHERE a NOT IN (?, ?) AND b IN (?, ?, ?) ``` +* 在一个Go程中多次操作数据库,但没有事务 + +```Go +session := engine.NewSession() +defer session.Close() + +user1 := Userinfo{Username: "xiaoxiao", Departname: "dev", Alias: "lunny", Created: time.Now()} +if _, err := session.Insert(&user1); err != nil { + return err +} + +user2 := Userinfo{Username: "yyy"} +if _, err := session.Where("id = ?", 2).Update(&user2); err != nil { + return err +} + +if _, err := session.Exec("delete from userinfo where username = ?", user2.Username); err != nil { + return err +} + +return nil +``` + +* 在一个Go程中有事务 + +```Go +session := engine.NewSession() +defer session.Close() + +// add Begin() before any action +if err := session.Begin(); err != nil { + // if returned then will rollback automatically + return err +} + +user1 := Userinfo{Username: "xiaoxiao", Departname: "dev", Alias: "lunny", Created: time.Now()} +if _, err := session.Insert(&user1); err != nil { + return err +} + +user2 := Userinfo{Username: "yyy"} +if _, err := session.Where("id = ?", 2).Update(&user2); err != nil { + return err +} + +if _, err := session.Exec("delete from userinfo where username = ?", user2.Username); err != nil { + return err +} + +// add Commit() after all actions +return session.Commit() +``` + # 案例 * [Go语言中文网](http://studygolang.com/) - [github.com/studygolang/studygolang](https://github.com/studygolang/studygolang) diff --git a/vendor/github.com/go-xorm/xorm/context.go b/vendor/github.com/go-xorm/xorm/context.go new file mode 100644 index 0000000000..074ba35a80 --- /dev/null +++ b/vendor/github.com/go-xorm/xorm/context.go @@ -0,0 +1,26 @@ +// Copyright 2017 The Xorm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build go1.8 + +package xorm + +import "context" + +// PingContext tests if database is alive +func (engine *Engine) PingContext(ctx context.Context) error { + session := engine.NewSession() + defer session.Close() + return session.PingContext(ctx) +} + +// PingContext test if database is ok +func (session *Session) PingContext(ctx context.Context) error { + if session.isAutoClose { + defer session.Close() + } + + session.engine.logger.Infof("PING DATABASE %v", session.engine.DriverName()) + return session.DB().PingContext(ctx) +} diff --git a/vendor/github.com/go-xorm/xorm/convert.go b/vendor/github.com/go-xorm/xorm/convert.go index 0504bef155..2316ca0b4d 100644 --- a/vendor/github.com/go-xorm/xorm/convert.go +++ b/vendor/github.com/go-xorm/xorm/convert.go @@ -209,10 +209,10 @@ func convertAssign(dest, src interface{}) error { if src == nil { dv.Set(reflect.Zero(dv.Type())) return nil - } else { - dv.Set(reflect.New(dv.Type().Elem())) - return convertAssign(dv.Interface(), src) } + + dv.Set(reflect.New(dv.Type().Elem())) + return convertAssign(dv.Interface(), src) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: s := asString(src) i64, err := strconv.ParseInt(s, 10, dv.Type().Bits()) diff --git a/vendor/github.com/go-xorm/xorm/dialect_postgres.go b/vendor/github.com/go-xorm/xorm/dialect_postgres.go index 3f5c526f72..d6f71acc1d 100644 --- a/vendor/github.com/go-xorm/xorm/dialect_postgres.go +++ b/vendor/github.com/go-xorm/xorm/dialect_postgres.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "net/url" - "sort" "strconv" "strings" @@ -765,13 +764,18 @@ var ( "YES": true, "ZONE": true, } + + // DefaultPostgresSchema default postgres schema + DefaultPostgresSchema = "public" ) type postgres struct { core.Base + schema string } func (db *postgres) Init(d *core.DB, uri *core.Uri, drivername, dataSourceName string) error { + db.schema = DefaultPostgresSchema return db.Base.Init(d, db, uri, drivername, dataSourceName) } @@ -923,7 +927,7 @@ func (db *postgres) IsColumnExist(tableName, colName string) (bool, error) { func (db *postgres) GetColumns(tableName string) ([]string, map[string]*core.Column, error) { // FIXME: the schema should be replaced by user custom's - args := []interface{}{tableName, "public"} + args := []interface{}{tableName, db.schema} s := `SELECT column_name, column_default, is_nullable, data_type, character_maximum_length, numeric_precision, numeric_precision_radix , CASE WHEN p.contype = 'p' THEN true ELSE false END AS primarykey, CASE WHEN p.contype = 'u' THEN true ELSE false END AS uniquekey @@ -1024,8 +1028,7 @@ WHERE c.relkind = 'r'::char AND c.relname = $1 AND s.table_schema = $2 AND f.att } func (db *postgres) GetTables() ([]*core.Table, error) { - // FIXME: replace public to user customrize schema - args := []interface{}{"public"} + args := []interface{}{db.schema} s := fmt.Sprintf("SELECT tablename FROM pg_tables WHERE schemaname = $1") db.LogSQL(s, args) @@ -1050,8 +1053,7 @@ func (db *postgres) GetTables() ([]*core.Table, error) { } func (db *postgres) GetIndexes(tableName string) (map[string]*core.Index, error) { - // FIXME: replace the public schema to user specify schema - args := []interface{}{"public", tableName} + args := []interface{}{db.schema, tableName} s := fmt.Sprintf("SELECT indexname, indexdef FROM pg_indexes WHERE schemaname=$1 AND tablename=$2") db.LogSQL(s, args) @@ -1117,10 +1119,6 @@ func (vs values) Get(k string) (v string) { return vs[k] } -func errorf(s string, args ...interface{}) { - panic(fmt.Errorf("pq: %s", fmt.Sprintf(s, args...))) -} - func parseURL(connstr string) (string, error) { u, err := url.Parse(connstr) if err != nil { @@ -1131,46 +1129,18 @@ func parseURL(connstr string) (string, error) { return "", fmt.Errorf("invalid connection protocol: %s", u.Scheme) } - var kvs []string escaper := strings.NewReplacer(` `, `\ `, `'`, `\'`, `\`, `\\`) - accrue := func(k, v string) { - if v != "" { - kvs = append(kvs, k+"="+escaper.Replace(v)) - } - } - - if u.User != nil { - v := u.User.Username() - accrue("user", v) - - v, _ = u.User.Password() - accrue("password", v) - } - - i := strings.Index(u.Host, ":") - if i < 0 { - accrue("host", u.Host) - } else { - accrue("host", u.Host[:i]) - accrue("port", u.Host[i+1:]) - } if u.Path != "" { - accrue("dbname", u.Path[1:]) + return escaper.Replace(u.Path[1:]), nil } - q := u.Query() - for k := range q { - accrue(k, q.Get(k)) - } - - sort.Strings(kvs) // Makes testing easier (not a performance concern) - return strings.Join(kvs, " "), nil + return "", nil } -func parseOpts(name string, o values) { +func parseOpts(name string, o values) error { if len(name) == 0 { - return + return fmt.Errorf("invalid options: %s", name) } name = strings.TrimSpace(name) @@ -1179,31 +1149,36 @@ func parseOpts(name string, o values) { for _, p := range ps { kv := strings.Split(p, "=") if len(kv) < 2 { - errorf("invalid option: %q", p) + return fmt.Errorf("invalid option: %q", p) } o.Set(kv[0], kv[1]) } + + return nil } func (p *pqDriver) Parse(driverName, dataSourceName string) (*core.Uri, error) { db := &core.Uri{DbType: core.POSTGRES} - o := make(values) var err error + if strings.HasPrefix(dataSourceName, "postgresql://") || strings.HasPrefix(dataSourceName, "postgres://") { - dataSourceName, err = parseURL(dataSourceName) + db.DbName, err = parseURL(dataSourceName) + if err != nil { + return nil, err + } + } else { + o := make(values) + err = parseOpts(dataSourceName, o) if err != nil { return nil, err } - } - parseOpts(dataSourceName, o) - db.DbName = o.Get("dbname") + db.DbName = o.Get("dbname") + } + if db.DbName == "" { return nil, errors.New("dbname is empty") } - /*db.Schema = o.Get("schema") - if len(db.Schema) == 0 { - db.Schema = "public" - }*/ + return db, nil } diff --git a/vendor/github.com/go-xorm/xorm/engine.go b/vendor/github.com/go-xorm/xorm/engine.go index 15c619d33b..444611afb1 100644 --- a/vendor/github.com/go-xorm/xorm/engine.go +++ b/vendor/github.com/go-xorm/xorm/engine.go @@ -47,6 +47,23 @@ type Engine struct { disableGlobalCache bool tagHandlers map[string]tagHandler + + engineGroup *EngineGroup +} + +// BufferSize sets buffer size for iterate +func (engine *Engine) BufferSize(size int) *Session { + session := engine.NewSession() + session.isAutoClose = true + return session.BufferSize(size) +} + +// CondDeleted returns the conditions whether a record is soft deleted. +func (engine *Engine) CondDeleted(colName string) builder.Cond { + if engine.dialect.DBType() == core.MSSQL { + return builder.IsNull{colName} + } + return builder.IsNull{colName}.Or(builder.Eq{colName: zeroTime1}) } // ShowSQL show SQL statement or not on logger if log level is great than INFO @@ -79,6 +96,11 @@ func (engine *Engine) SetLogger(logger core.ILogger) { engine.dialect.SetLogger(logger) } +// SetLogLevel sets the logger level +func (engine *Engine) SetLogLevel(level core.LogLevel) { + engine.logger.SetLevel(level) +} + // SetDisableGlobalCache disable global cache or not func (engine *Engine) SetDisableGlobalCache(disable bool) { if engine.disableGlobalCache != disable { @@ -201,6 +223,11 @@ func (engine *Engine) SetDefaultCacher(cacher core.Cacher) { engine.Cacher = cacher } +// GetDefaultCacher returns the default cacher +func (engine *Engine) GetDefaultCacher() core.Cacher { + return engine.Cacher +} + // NoCache If you has set default cacher, and you want temporilly stop use cache, // you can use NoCache() func (engine *Engine) NoCache() *Session { @@ -736,6 +763,13 @@ func (engine *Engine) OrderBy(order string) *Session { return session.OrderBy(order) } +// Prepare enables prepare statement +func (engine *Engine) Prepare() *Session { + session := engine.NewSession() + session.isAutoClose = true + return session.Prepare() +} + // Join the join_operator should be one of INNER, LEFT OUTER, CROSS etc - this will be prepended to JOIN func (engine *Engine) Join(joinOperator string, tablename interface{}, condition string, args ...interface{}) *Session { session := engine.NewSession() @@ -757,7 +791,8 @@ func (engine *Engine) Having(conditions string) *Session { return session.Having(conditions) } -func (engine *Engine) unMapType(t reflect.Type) { +// UnMapType removes the datbase mapper of a type +func (engine *Engine) UnMapType(t reflect.Type) { engine.mutex.Lock() defer engine.mutex.Unlock() delete(engine.Tables, t) @@ -914,7 +949,7 @@ func (engine *Engine) mapType(v reflect.Value) (*core.Table, error) { } if pStart > -1 { if !strings.HasSuffix(k, ")") { - return nil, errors.New("cannot match ) charactor") + return nil, fmt.Errorf("field %s tag %s cannot match ) charactor", col.FieldName, key) } ctx.tagName = k[:pStart] @@ -1341,24 +1376,24 @@ func (engine *Engine) Exec(sql string, args ...interface{}) (sql.Result, error) } // Query a raw sql and return records as []map[string][]byte -func (engine *Engine) Query(sql string, paramStr ...interface{}) (resultsSlice []map[string][]byte, err error) { +func (engine *Engine) Query(sqlorArgs ...interface{}) (resultsSlice []map[string][]byte, err error) { session := engine.NewSession() defer session.Close() - return session.Query(sql, paramStr...) + return session.Query(sqlorArgs...) } // QueryString runs a raw sql and return records as []map[string]string -func (engine *Engine) QueryString(sqlStr string, args ...interface{}) ([]map[string]string, error) { +func (engine *Engine) QueryString(sqlorArgs ...interface{}) ([]map[string]string, error) { session := engine.NewSession() defer session.Close() - return session.QueryString(sqlStr, args...) + return session.QueryString(sqlorArgs...) } // QueryInterface runs a raw sql and return records as []map[string]interface{} -func (engine *Engine) QueryInterface(sqlStr string, args ...interface{}) ([]map[string]interface{}, error) { +func (engine *Engine) QueryInterface(sqlorArgs ...interface{}) ([]map[string]interface{}, error) { session := engine.NewSession() defer session.Close() - return session.QueryInterface(sqlStr, args...) + return session.QueryInterface(sqlorArgs...) } // Insert one or more records @@ -1564,24 +1599,39 @@ func (engine *Engine) formatTime(sqlTypeName string, t time.Time) (v interface{} return } +// GetColumnMapper returns the column name mapper +func (engine *Engine) GetColumnMapper() core.IMapper { + return engine.ColumnMapper +} + +// GetTableMapper returns the table name mapper +func (engine *Engine) GetTableMapper() core.IMapper { + return engine.TableMapper +} + +// GetTZLocation returns time zone of the application +func (engine *Engine) GetTZLocation() *time.Location { + return engine.TZLocation +} + +// SetTZLocation sets time zone of the application +func (engine *Engine) SetTZLocation(tz *time.Location) { + engine.TZLocation = tz +} + +// GetTZDatabase returns time zone of the database +func (engine *Engine) GetTZDatabase() *time.Location { + return engine.DatabaseTZ +} + +// SetTZDatabase sets time zone of the database +func (engine *Engine) SetTZDatabase(tz *time.Location) { + engine.DatabaseTZ = tz +} + // Unscoped always disable struct tag "deleted" func (engine *Engine) Unscoped() *Session { session := engine.NewSession() session.isAutoClose = true return session.Unscoped() } - -// CondDeleted returns the conditions whether a record is soft deleted. -func (engine *Engine) CondDeleted(colName string) builder.Cond { - if engine.dialect.DBType() == core.MSSQL { - return builder.IsNull{colName} - } - return builder.IsNull{colName}.Or(builder.Eq{colName: zeroTime1}) -} - -// BufferSize sets buffer size for iterate -func (engine *Engine) BufferSize(size int) *Session { - session := engine.NewSession() - session.isAutoClose = true - return session.BufferSize(size) -} diff --git a/vendor/github.com/go-xorm/xorm/engine_group.go b/vendor/github.com/go-xorm/xorm/engine_group.go new file mode 100644 index 0000000000..1de425f372 --- /dev/null +++ b/vendor/github.com/go-xorm/xorm/engine_group.go @@ -0,0 +1,194 @@ +// Copyright 2017 The Xorm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package xorm + +import ( + "github.com/go-xorm/core" +) + +// EngineGroup defines an engine group +type EngineGroup struct { + *Engine + slaves []*Engine + policy GroupPolicy +} + +// NewEngineGroup creates a new engine group +func NewEngineGroup(args1 interface{}, args2 interface{}, policies ...GroupPolicy) (*EngineGroup, error) { + var eg EngineGroup + if len(policies) > 0 { + eg.policy = policies[0] + } else { + eg.policy = RoundRobinPolicy() + } + + driverName, ok1 := args1.(string) + conns, ok2 := args2.([]string) + if ok1 && ok2 { + engines := make([]*Engine, len(conns)) + for i, conn := range conns { + engine, err := NewEngine(driverName, conn) + if err != nil { + return nil, err + } + engine.engineGroup = &eg + engines[i] = engine + } + + eg.Engine = engines[0] + eg.slaves = engines[1:] + return &eg, nil + } + + master, ok3 := args1.(*Engine) + slaves, ok4 := args2.([]*Engine) + if ok3 && ok4 { + master.engineGroup = &eg + for i := 0; i < len(slaves); i++ { + slaves[i].engineGroup = &eg + } + eg.Engine = master + eg.slaves = slaves + return &eg, nil + } + return nil, ErrParamsType +} + +// Close the engine +func (eg *EngineGroup) Close() error { + err := eg.Engine.Close() + if err != nil { + return err + } + + for i := 0; i < len(eg.slaves); i++ { + err := eg.slaves[i].Close() + if err != nil { + return err + } + } + return nil +} + +// Master returns the master engine +func (eg *EngineGroup) Master() *Engine { + return eg.Engine +} + +// Ping tests if database is alive +func (eg *EngineGroup) Ping() error { + if err := eg.Engine.Ping(); err != nil { + return err + } + + for _, slave := range eg.slaves { + if err := slave.Ping(); err != nil { + return err + } + } + return nil +} + +// SetColumnMapper set the column name mapping rule +func (eg *EngineGroup) SetColumnMapper(mapper core.IMapper) { + eg.Engine.ColumnMapper = mapper + for i := 0; i < len(eg.slaves); i++ { + eg.slaves[i].ColumnMapper = mapper + } +} + +// SetDefaultCacher set the default cacher +func (eg *EngineGroup) SetDefaultCacher(cacher core.Cacher) { + eg.Engine.SetDefaultCacher(cacher) + for i := 0; i < len(eg.slaves); i++ { + eg.slaves[i].SetDefaultCacher(cacher) + } +} + +// SetLogger set the new logger +func (eg *EngineGroup) SetLogger(logger core.ILogger) { + eg.Engine.SetLogger(logger) + for i := 0; i < len(eg.slaves); i++ { + eg.slaves[i].SetLogger(logger) + } +} + +// SetLogLevel sets the logger level +func (eg *EngineGroup) SetLogLevel(level core.LogLevel) { + eg.Engine.SetLogLevel(level) + for i := 0; i < len(eg.slaves); i++ { + eg.slaves[i].SetLogLevel(level) + } +} + +// SetMapper set the name mapping rules +func (eg *EngineGroup) SetMapper(mapper core.IMapper) { + eg.Engine.SetMapper(mapper) + for i := 0; i < len(eg.slaves); i++ { + eg.slaves[i].SetMapper(mapper) + } +} + +// SetMaxIdleConns set the max idle connections on pool, default is 2 +func (eg *EngineGroup) SetMaxIdleConns(conns int) { + eg.Engine.db.SetMaxIdleConns(conns) + for i := 0; i < len(eg.slaves); i++ { + eg.slaves[i].db.SetMaxIdleConns(conns) + } +} + +// SetMaxOpenConns is only available for go 1.2+ +func (eg *EngineGroup) SetMaxOpenConns(conns int) { + eg.Engine.db.SetMaxOpenConns(conns) + for i := 0; i < len(eg.slaves); i++ { + eg.slaves[i].db.SetMaxOpenConns(conns) + } +} + +// SetPolicy set the group policy +func (eg *EngineGroup) SetPolicy(policy GroupPolicy) *EngineGroup { + eg.policy = policy + return eg +} + +// SetTableMapper set the table name mapping rule +func (eg *EngineGroup) SetTableMapper(mapper core.IMapper) { + eg.Engine.TableMapper = mapper + for i := 0; i < len(eg.slaves); i++ { + eg.slaves[i].TableMapper = mapper + } +} + +// ShowExecTime show SQL statement and execute time or not on logger if log level is great than INFO +func (eg *EngineGroup) ShowExecTime(show ...bool) { + eg.Engine.ShowExecTime(show...) + for i := 0; i < len(eg.slaves); i++ { + eg.slaves[i].ShowExecTime(show...) + } +} + +// ShowSQL show SQL statement or not on logger if log level is great than INFO +func (eg *EngineGroup) ShowSQL(show ...bool) { + eg.Engine.ShowSQL(show...) + for i := 0; i < len(eg.slaves); i++ { + eg.slaves[i].ShowSQL(show...) + } +} + +// Slave returns one of the physical databases which is a slave according the policy +func (eg *EngineGroup) Slave() *Engine { + switch len(eg.slaves) { + case 0: + return eg.Engine + case 1: + return eg.slaves[0] + } + return eg.policy.Slave(eg) +} + +// Slaves returns all the slaves +func (eg *EngineGroup) Slaves() []*Engine { + return eg.slaves +} diff --git a/vendor/github.com/go-xorm/xorm/engine_group_policy.go b/vendor/github.com/go-xorm/xorm/engine_group_policy.go new file mode 100644 index 0000000000..5b56e8995f --- /dev/null +++ b/vendor/github.com/go-xorm/xorm/engine_group_policy.go @@ -0,0 +1,116 @@ +// Copyright 2017 The Xorm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package xorm + +import ( + "math/rand" + "sync" + "time" +) + +// GroupPolicy is be used by chosing the current slave from slaves +type GroupPolicy interface { + Slave(*EngineGroup) *Engine +} + +// GroupPolicyHandler should be used when a function is a GroupPolicy +type GroupPolicyHandler func(*EngineGroup) *Engine + +// Slave implements the chosen of slaves +func (h GroupPolicyHandler) Slave(eg *EngineGroup) *Engine { + return h(eg) +} + +// RandomPolicy implmentes randomly chose the slave of slaves +func RandomPolicy() GroupPolicyHandler { + var r = rand.New(rand.NewSource(time.Now().UnixNano())) + return func(g *EngineGroup) *Engine { + return g.Slaves()[r.Intn(len(g.Slaves()))] + } +} + +// WeightRandomPolicy implmentes randomly chose the slave of slaves +func WeightRandomPolicy(weights []int) GroupPolicyHandler { + var rands = make([]int, 0, len(weights)) + for i := 0; i < len(weights); i++ { + for n := 0; n < weights[i]; n++ { + rands = append(rands, i) + } + } + var r = rand.New(rand.NewSource(time.Now().UnixNano())) + + return func(g *EngineGroup) *Engine { + var slaves = g.Slaves() + idx := rands[r.Intn(len(rands))] + if idx >= len(slaves) { + idx = len(slaves) - 1 + } + return slaves[idx] + } +} + +func RoundRobinPolicy() GroupPolicyHandler { + var pos = -1 + var lock sync.Mutex + return func(g *EngineGroup) *Engine { + var slaves = g.Slaves() + + lock.Lock() + defer lock.Unlock() + pos++ + if pos >= len(slaves) { + pos = 0 + } + + return slaves[pos] + } +} + +func WeightRoundRobinPolicy(weights []int) GroupPolicyHandler { + var rands = make([]int, 0, len(weights)) + for i := 0; i < len(weights); i++ { + for n := 0; n < weights[i]; n++ { + rands = append(rands, i) + } + } + var pos = -1 + var lock sync.Mutex + + return func(g *EngineGroup) *Engine { + var slaves = g.Slaves() + lock.Lock() + defer lock.Unlock() + pos++ + if pos >= len(rands) { + pos = 0 + } + + idx := rands[pos] + if idx >= len(slaves) { + idx = len(slaves) - 1 + } + return slaves[idx] + } +} + +// LeastConnPolicy implements GroupPolicy, every time will get the least connections slave +func LeastConnPolicy() GroupPolicyHandler { + return func(g *EngineGroup) *Engine { + var slaves = g.Slaves() + connections := 0 + idx := 0 + for i := 0; i < len(slaves); i++ { + openConnections := slaves[i].DB().Stats().OpenConnections + if i == 0 { + connections = openConnections + idx = i + } else if openConnections <= connections { + connections = openConnections + idx = i + } + } + return slaves[idx] + } +} diff --git a/vendor/github.com/go-xorm/xorm/engine_maxlife.go b/vendor/github.com/go-xorm/xorm/engine_maxlife.go index 21daeaa1b8..22666c5f44 100644 --- a/vendor/github.com/go-xorm/xorm/engine_maxlife.go +++ b/vendor/github.com/go-xorm/xorm/engine_maxlife.go @@ -12,3 +12,11 @@ import "time" func (engine *Engine) SetConnMaxLifetime(d time.Duration) { engine.db.SetConnMaxLifetime(d) } + +// SetConnMaxLifetime sets the maximum amount of time a connection may be reused. +func (eg *EngineGroup) SetConnMaxLifetime(d time.Duration) { + eg.Engine.SetConnMaxLifetime(d) + for i := 0; i < len(eg.slaves); i++ { + eg.slaves[i].SetConnMaxLifetime(d) + } +} diff --git a/vendor/github.com/go-xorm/xorm/error.go b/vendor/github.com/go-xorm/xorm/error.go index 2a334f47c2..cfeefc31e8 100644 --- a/vendor/github.com/go-xorm/xorm/error.go +++ b/vendor/github.com/go-xorm/xorm/error.go @@ -23,4 +23,6 @@ var ( ErrNeedDeletedCond = errors.New("Delete need at least one condition") // ErrNotImplemented not implemented ErrNotImplemented = errors.New("Not implemented") + // ErrConditionType condition type unsupported + ErrConditionType = errors.New("Unsupported conditon type") ) diff --git a/vendor/github.com/go-xorm/xorm/interface.go b/vendor/github.com/go-xorm/xorm/interface.go new file mode 100644 index 0000000000..9a3b6da0b2 --- /dev/null +++ b/vendor/github.com/go-xorm/xorm/interface.go @@ -0,0 +1,103 @@ +// Copyright 2017 The Xorm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package xorm + +import ( + "database/sql" + "reflect" + "time" + + "github.com/go-xorm/core" +) + +// Interface defines the interface which Engine, EngineGroup and Session will implementate. +type Interface interface { + AllCols() *Session + Alias(alias string) *Session + Asc(colNames ...string) *Session + BufferSize(size int) *Session + Cols(columns ...string) *Session + Count(...interface{}) (int64, error) + CreateIndexes(bean interface{}) error + CreateUniques(bean interface{}) error + Decr(column string, arg ...interface{}) *Session + Desc(...string) *Session + Delete(interface{}) (int64, error) + Distinct(columns ...string) *Session + DropIndexes(bean interface{}) error + Exec(string, ...interface{}) (sql.Result, error) + Exist(bean ...interface{}) (bool, error) + Find(interface{}, ...interface{}) error + Get(interface{}) (bool, error) + GroupBy(keys string) *Session + ID(interface{}) *Session + In(string, ...interface{}) *Session + Incr(column string, arg ...interface{}) *Session + Insert(...interface{}) (int64, error) + InsertOne(interface{}) (int64, error) + IsTableEmpty(bean interface{}) (bool, error) + IsTableExist(beanOrTableName interface{}) (bool, error) + Iterate(interface{}, IterFunc) error + Limit(int, ...int) *Session + NoAutoCondition(...bool) *Session + NotIn(string, ...interface{}) *Session + Join(joinOperator string, tablename interface{}, condition string, args ...interface{}) *Session + Omit(columns ...string) *Session + OrderBy(order string) *Session + Ping() error + Query(sqlOrAgrs ...interface{}) (resultsSlice []map[string][]byte, err error) + QueryInterface(sqlorArgs ...interface{}) ([]map[string]interface{}, error) + QueryString(sqlorArgs ...interface{}) ([]map[string]string, error) + Rows(bean interface{}) (*Rows, error) + SetExpr(string, string) *Session + SQL(interface{}, ...interface{}) *Session + Sum(bean interface{}, colName string) (float64, error) + SumInt(bean interface{}, colName string) (int64, error) + Sums(bean interface{}, colNames ...string) ([]float64, error) + SumsInt(bean interface{}, colNames ...string) ([]int64, error) + Table(tableNameOrBean interface{}) *Session + Unscoped() *Session + Update(bean interface{}, condiBeans ...interface{}) (int64, error) + UseBool(...string) *Session + Where(interface{}, ...interface{}) *Session +} + +// EngineInterface defines the interface which Engine, EngineGroup will implementate. +type EngineInterface interface { + Interface + + Before(func(interface{})) *Session + Charset(charset string) *Session + CreateTables(...interface{}) error + DBMetas() ([]*core.Table, error) + Dialect() core.Dialect + DropTables(...interface{}) error + DumpAllToFile(fp string, tp ...core.DbType) error + GetColumnMapper() core.IMapper + GetDefaultCacher() core.Cacher + GetTableMapper() core.IMapper + GetTZDatabase() *time.Location + GetTZLocation() *time.Location + NewSession() *Session + NoAutoTime() *Session + Quote(string) string + SetDefaultCacher(core.Cacher) + SetLogLevel(core.LogLevel) + SetMapper(core.IMapper) + SetTZDatabase(tz *time.Location) + SetTZLocation(tz *time.Location) + ShowSQL(show ...bool) + Sync(...interface{}) error + Sync2(...interface{}) error + StoreEngine(storeEngine string) *Session + TableInfo(bean interface{}) *Table + UnMapType(reflect.Type) +} + +var ( + _ Interface = &Session{} + _ EngineInterface = &Engine{} + _ EngineInterface = &EngineGroup{} +) diff --git a/vendor/github.com/go-xorm/xorm/session.go b/vendor/github.com/go-xorm/xorm/session.go index ed25205880..5c6cb5f9de 100644 --- a/vendor/github.com/go-xorm/xorm/session.go +++ b/vendor/github.com/go-xorm/xorm/session.go @@ -76,6 +76,7 @@ func (session *Session) Init() { session.afterDeleteBeans = make(map[interface{}]*[]func(interface{}), 0) session.beforeClosures = make([]func(interface{}), 0) session.afterClosures = make([]func(interface{}), 0) + session.stmtCache = make(map[uint32]*core.Stmt) session.afterProcessors = make([]executedProcessor, 0) @@ -262,13 +263,13 @@ func (session *Session) canCache() bool { return true } -func (session *Session) doPrepare(sqlStr string) (stmt *core.Stmt, err error) { +func (session *Session) doPrepare(db *core.DB, sqlStr string) (stmt *core.Stmt, err error) { crc := crc32.ChecksumIEEE([]byte(sqlStr)) // TODO try hash(sqlStr+len(sqlStr)) var has bool stmt, has = session.stmtCache[crc] if !has { - stmt, err = session.DB().Prepare(sqlStr) + stmt, err = db.Prepare(sqlStr) if err != nil { return nil, err } @@ -461,6 +462,10 @@ func (session *Session) slice2Bean(scanResults []interface{}, fields []string, b hasAssigned = true if len(bs) > 0 { + if fieldType.Kind() == reflect.String { + fieldValue.SetString(string(bs)) + continue + } if fieldValue.CanAddr() { err := json.Unmarshal(bs, fieldValue.Addr().Interface()) if err != nil { diff --git a/vendor/github.com/go-xorm/xorm/session_convert.go b/vendor/github.com/go-xorm/xorm/session_convert.go index f2c949bac8..1f9d8aa1bd 100644 --- a/vendor/github.com/go-xorm/xorm/session_convert.go +++ b/vendor/github.com/go-xorm/xorm/session_convert.go @@ -34,27 +34,27 @@ func (session *Session) str2Time(col *core.Column, data string) (outTime time.Ti sd, err := strconv.ParseInt(sdata, 10, 64) if err == nil { x = time.Unix(sd, 0) - session.engine.logger.Debugf("time(0) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) + //session.engine.logger.Debugf("time(0) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) } else { - session.engine.logger.Debugf("time(0) err key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) + //session.engine.logger.Debugf("time(0) err key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) } } else if len(sdata) > 19 && strings.Contains(sdata, "-") { x, err = time.ParseInLocation(time.RFC3339Nano, sdata, parseLoc) session.engine.logger.Debugf("time(1) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) if err != nil { x, err = time.ParseInLocation("2006-01-02 15:04:05.999999999", sdata, parseLoc) - session.engine.logger.Debugf("time(2) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) + //session.engine.logger.Debugf("time(2) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) } if err != nil { x, err = time.ParseInLocation("2006-01-02 15:04:05.9999999 Z07:00", sdata, parseLoc) - session.engine.logger.Debugf("time(3) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) + //session.engine.logger.Debugf("time(3) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) } } else if len(sdata) == 19 && strings.Contains(sdata, "-") { x, err = time.ParseInLocation("2006-01-02 15:04:05", sdata, parseLoc) - session.engine.logger.Debugf("time(4) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) + //session.engine.logger.Debugf("time(4) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) } else if len(sdata) == 10 && sdata[4] == '-' && sdata[7] == '-' { x, err = time.ParseInLocation("2006-01-02", sdata, parseLoc) - session.engine.logger.Debugf("time(5) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) + //session.engine.logger.Debugf("time(5) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) } else if col.SQLType.Name == core.Time { if strings.Contains(sdata, " ") { ssd := strings.Split(sdata, " ") @@ -68,7 +68,7 @@ func (session *Session) str2Time(col *core.Column, data string) (outTime time.Ti st := fmt.Sprintf("2006-01-02 %v", sdata) x, err = time.ParseInLocation("2006-01-02 15:04:05", st, parseLoc) - session.engine.logger.Debugf("time(6) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) + //session.engine.logger.Debugf("time(6) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) } else { outErr = fmt.Errorf("unsupported time format %v", sdata) return diff --git a/vendor/github.com/go-xorm/xorm/session_exist.go b/vendor/github.com/go-xorm/xorm/session_exist.go index 049c1ddff1..378a648376 100644 --- a/vendor/github.com/go-xorm/xorm/session_exist.go +++ b/vendor/github.com/go-xorm/xorm/session_exist.go @@ -10,6 +10,7 @@ import ( "reflect" "github.com/go-xorm/builder" + "github.com/go-xorm/core" ) // Exist returns true if the record exist otherwise return false @@ -35,10 +36,18 @@ func (session *Session) Exist(bean ...interface{}) (bool, error) { return false, err } - sqlStr = fmt.Sprintf("SELECT * FROM %s WHERE %s LIMIT 1", tableName, condSQL) + if session.engine.dialect.DBType() == core.MSSQL { + sqlStr = fmt.Sprintf("SELECT top 1 * FROM %s WHERE %s", tableName, condSQL) + } else { + sqlStr = fmt.Sprintf("SELECT * FROM %s WHERE %s LIMIT 1", tableName, condSQL) + } args = condArgs } else { - sqlStr = fmt.Sprintf("SELECT * FROM %s LIMIT 1", tableName) + if session.engine.dialect.DBType() == core.MSSQL { + sqlStr = fmt.Sprintf("SELECT top 1 * FROM %s", tableName) + } else { + sqlStr = fmt.Sprintf("SELECT * FROM %s LIMIT 1", tableName) + } args = []interface{}{} } } else { diff --git a/vendor/github.com/go-xorm/xorm/session_get.go b/vendor/github.com/go-xorm/xorm/session_get.go index 8faf53c02c..68b37af7f1 100644 --- a/vendor/github.com/go-xorm/xorm/session_get.go +++ b/vendor/github.com/go-xorm/xorm/session_get.go @@ -5,6 +5,7 @@ package xorm import ( + "database/sql" "errors" "reflect" "strconv" @@ -79,6 +80,13 @@ func (session *Session) nocacheGet(beanKind reflect.Kind, table *core.Table, bea return false, nil } + switch bean.(type) { + case sql.NullInt64, sql.NullBool, sql.NullFloat64, sql.NullString: + return true, rows.Scan(&bean) + case *sql.NullInt64, *sql.NullBool, *sql.NullFloat64, *sql.NullString: + return true, rows.Scan(bean) + } + switch beanKind { case reflect.Struct: fields, err := rows.Columns() diff --git a/vendor/github.com/go-xorm/xorm/session_insert.go b/vendor/github.com/go-xorm/xorm/session_insert.go index 478501f0b2..129ee23098 100644 --- a/vendor/github.com/go-xorm/xorm/session_insert.go +++ b/vendor/github.com/go-xorm/xorm/session_insert.go @@ -400,7 +400,7 @@ func (session *Session) innerInsert(bean interface{}) (int64, error) { return 0, err } - handleAfterInsertProcessorFunc(bean) + defer handleAfterInsertProcessorFunc(bean) if cacher := session.engine.getCacher2(table); cacher != nil && session.statement.UseCache { session.cacheInsert(table, tableName) @@ -445,7 +445,7 @@ func (session *Session) innerInsert(bean interface{}) (int64, error) { if err != nil { return 0, err } - handleAfterInsertProcessorFunc(bean) + defer handleAfterInsertProcessorFunc(bean) if cacher := session.engine.getCacher2(table); cacher != nil && session.statement.UseCache { session.cacheInsert(table, tableName) diff --git a/vendor/github.com/go-xorm/xorm/session_query.go b/vendor/github.com/go-xorm/xorm/session_query.go index a693bace3f..f8098f849c 100644 --- a/vendor/github.com/go-xorm/xorm/session_query.go +++ b/vendor/github.com/go-xorm/xorm/session_query.go @@ -8,17 +8,92 @@ import ( "fmt" "reflect" "strconv" + "strings" "time" + "github.com/go-xorm/builder" "github.com/go-xorm/core" ) +func (session *Session) genQuerySQL(sqlorArgs ...interface{}) (string, []interface{}, error) { + if len(sqlorArgs) > 0 { + switch sqlorArgs[0].(type) { + case string: + return sqlorArgs[0].(string), sqlorArgs[1:], nil + case *builder.Builder: + return sqlorArgs[0].(*builder.Builder).ToSQL() + case builder.Builder: + bd := sqlorArgs[0].(builder.Builder) + return bd.ToSQL() + default: + return "", nil, ErrUnSupportedType + } + } + + if session.statement.RawSQL != "" { + return session.statement.RawSQL, session.statement.RawParams, nil + } + + if len(session.statement.TableName()) <= 0 { + return "", nil, ErrTableNotFound + } + + var columnStr = session.statement.ColumnStr + if len(session.statement.selectStr) > 0 { + columnStr = session.statement.selectStr + } else { + if session.statement.JoinStr == "" { + if columnStr == "" { + if session.statement.GroupByStr != "" { + columnStr = session.statement.Engine.Quote(strings.Replace(session.statement.GroupByStr, ",", session.engine.Quote(","), -1)) + } else { + columnStr = session.statement.genColumnStr() + } + } + } else { + if columnStr == "" { + if session.statement.GroupByStr != "" { + columnStr = session.statement.Engine.Quote(strings.Replace(session.statement.GroupByStr, ",", session.engine.Quote(","), -1)) + } else { + columnStr = "*" + } + } + } + if columnStr == "" { + columnStr = "*" + } + } + + condSQL, condArgs, err := builder.ToSQL(session.statement.cond) + if err != nil { + return "", nil, err + } + + args := append(session.statement.joinArgs, condArgs...) + sqlStr, err := session.statement.genSelectSQL(columnStr, condSQL) + if err != nil { + return "", nil, err + } + // for mssql and use limit + qs := strings.Count(sqlStr, "?") + if len(args)*2 == qs { + args = append(args, args...) + } + + return sqlStr, args, nil +} + // Query runs a raw sql and return records as []map[string][]byte -func (session *Session) Query(sqlStr string, args ...interface{}) ([]map[string][]byte, error) { +func (session *Session) Query(sqlorArgs ...interface{}) ([]map[string][]byte, error) { if session.isAutoClose { defer session.Close() } + sqlStr, args, err := session.genQuerySQL(sqlorArgs...) + if err != nil { + return nil, err + } + return session.queryBytes(sqlStr, args...) } @@ -114,11 +189,16 @@ func rows2Strings(rows *core.Rows) (resultsSlice []map[string]string, err error) } // QueryString runs a raw sql and return records as []map[string]string -func (session *Session) QueryString(sqlStr string, args ...interface{}) ([]map[string]string, error) { +func (session *Session) QueryString(sqlorArgs ...interface{}) ([]map[string]string, error) { if session.isAutoClose { defer session.Close() } + sqlStr, args, err := session.genQuerySQL(sqlorArgs...) + if err != nil { + return nil, err + } + rows, err := session.queryRows(sqlStr, args...) if err != nil { return nil, err @@ -162,11 +242,16 @@ func rows2Interfaces(rows *core.Rows) (resultsSlice []map[string]interface{}, er } // QueryInterface runs a raw sql and return records as []map[string]interface{} -func (session *Session) QueryInterface(sqlStr string, args ...interface{}) ([]map[string]interface{}, error) { +func (session *Session) QueryInterface(sqlorArgs ...interface{}) ([]map[string]interface{}, error) { if session.isAutoClose { defer session.Close() } + sqlStr, args, err := session.genQuerySQL(sqlorArgs...) + if err != nil { + return nil, err + } + rows, err := session.queryRows(sqlStr, args...) if err != nil { return nil, err diff --git a/vendor/github.com/go-xorm/xorm/session_raw.go b/vendor/github.com/go-xorm/xorm/session_raw.go index c225598e60..69bf9b3c6b 100644 --- a/vendor/github.com/go-xorm/xorm/session_raw.go +++ b/vendor/github.com/go-xorm/xorm/session_raw.go @@ -47,9 +47,16 @@ func (session *Session) queryRows(sqlStr string, args ...interface{}) (*core.Row } if session.isAutoCommit { + var db *core.DB + if session.engine.engineGroup != nil { + db = session.engine.engineGroup.Slave().DB() + } else { + db = session.DB() + } + if session.prepareStmt { // don't clear stmt since session will cache them - stmt, err := session.doPrepare(sqlStr) + stmt, err := session.doPrepare(db, sqlStr) if err != nil { return nil, err } @@ -61,7 +68,7 @@ func (session *Session) queryRows(sqlStr string, args ...interface{}) (*core.Row return rows, nil } - rows, err := session.DB().Query(sqlStr, args...) + rows, err := db.Query(sqlStr, args...) if err != nil { return nil, err } @@ -171,7 +178,7 @@ func (session *Session) exec(sqlStr string, args ...interface{}) (sql.Result, er } if session.prepareStmt { - stmt, err := session.doPrepare(sqlStr) + stmt, err := session.doPrepare(session.DB(), sqlStr) if err != nil { return nil, err } diff --git a/vendor/github.com/go-xorm/xorm/session_update.go b/vendor/github.com/go-xorm/xorm/session_update.go index ca06298122..f558745667 100644 --- a/vendor/github.com/go-xorm/xorm/session_update.go +++ b/vendor/github.com/go-xorm/xorm/session_update.go @@ -242,10 +242,23 @@ func (session *Session) Update(bean interface{}, condiBean ...interface{}) (int6 var autoCond builder.Cond if !session.statement.noAutoCondition && len(condiBean) > 0 { - var err error - autoCond, err = session.statement.buildConds(session.statement.RefTable, condiBean[0], true, true, false, true, false) - if err != nil { - return 0, err + if c, ok := condiBean[0].(map[string]interface{}); ok { + autoCond = builder.Eq(c) + } else { + ct := reflect.TypeOf(condiBean[0]) + k := ct.Kind() + if k == reflect.Ptr { + k = ct.Elem().Kind() + } + if k == reflect.Struct { + var err error + autoCond, err = session.statement.buildConds(session.statement.RefTable, condiBean[0], true, true, false, true, false) + if err != nil { + return 0, err + } + } else { + return 0, ErrConditionType + } } } diff --git a/vendor/github.com/go-xorm/xorm/statement.go b/vendor/github.com/go-xorm/xorm/statement.go index 23346c7103..6400425b20 100644 --- a/vendor/github.com/go-xorm/xorm/statement.go +++ b/vendor/github.com/go-xorm/xorm/statement.go @@ -160,6 +160,9 @@ func (statement *Statement) And(query interface{}, args ...interface{}) *Stateme case string: cond := builder.Expr(query.(string), args...) statement.cond = statement.cond.And(cond) + case map[string]interface{}: + cond := builder.Eq(query.(map[string]interface{})) + statement.cond = statement.cond.And(cond) case builder.Cond: cond := query.(builder.Cond) statement.cond = statement.cond.And(cond) @@ -181,6 +184,9 @@ func (statement *Statement) Or(query interface{}, args ...interface{}) *Statemen case string: cond := builder.Expr(query.(string), args...) statement.cond = statement.cond.Or(cond) + case map[string]interface{}: + cond := builder.Eq(query.(map[string]interface{})) + statement.cond = statement.cond.Or(cond) case builder.Cond: cond := query.(builder.Cond) statement.cond = statement.cond.Or(cond) @@ -901,8 +907,12 @@ func (statement *Statement) genDelIndexSQL() []string { func (statement *Statement) genAddColumnStr(col *core.Column) (string, []interface{}) { quote := statement.Engine.Quote - sql := fmt.Sprintf("ALTER TABLE %v ADD %v;", quote(statement.TableName()), + sql := fmt.Sprintf("ALTER TABLE %v ADD %v", quote(statement.TableName()), col.String(statement.Engine.dialect)) + if statement.Engine.dialect.DBType() == core.MYSQL && len(col.Comment) > 0 { + sql += " COMMENT '" + col.Comment + "'" + } + sql += ";" return sql, []interface{}{} } diff --git a/vendor/vendor.json b/vendor/vendor.json index e008169262..a108bbbabb 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -462,16 +462,16 @@ "revisionTime": "2016-11-01T11:13:14Z" }, { - "checksumSHA1": "9SXbj96wb1PgppBZzxMIN0axbFQ=", + "checksumSHA1": "HsUSlgz1VKEEiZdkXY5qdLzexWU=", "path": "github.com/go-xorm/builder", - "revision": "c8871c857d2555fbfbd8524f895be5386d3d8836", - "revisionTime": "2017-05-19T03:21:30Z" + "revision": "488224409dd8aa2ce7a5baf8d10d55764a913738", + "revisionTime": "2018-01-16T06:54:19Z" }, { - "checksumSHA1": "HMavuxvDhKOwmbbFnYt9hfT6jE0=", + "checksumSHA1": "7JjlvSpGfLa49MHElks8NGBUfFA=", "path": "github.com/go-xorm/core", - "revision": "da1adaf7a28ca792961721a34e6e04945200c890", - "revisionTime": "2017-09-09T08:56:53Z" + "revision": "cb1d0ca71f42d3ee1bf4aba7daa16099bc31a7e9", + "revisionTime": "2017-12-21T01:38:49Z" }, { "checksumSHA1": "k52lEKLp8j5M+jFpe+3u+bIFpxQ=", @@ -480,10 +480,10 @@ "revisionTime": "2016-08-11T02:11:45Z" }, { - "checksumSHA1": "+KmPfckyKvrUZPIHBYHylg/7V8o=", + "checksumSHA1": "eGBz6F3I/0naVUclZ6GZWc3EzQo=", "path": "github.com/go-xorm/xorm", - "revision": "29d4a0330a00b9be468b70e3fb0f74109348c358", - "revisionTime": "2017-09-30T01:26:13Z" + "revision": "d4149d1eee0c2c488a74a5863fd9caf13d60fd03", + "revisionTime": "2018-01-22T13:32:35Z" }, { "checksumSHA1": "1ft/4j5MFa7C9dPI9whL03HSUzk=",