diff --git a/README.md b/README.md index 495d1dca..e53b3625 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ Yet Another ORM library for Go, aims for developer friendly * Transaction * Logger Support * Bind struct with tag -* Iteration Support via sql.Rows -* sql.Scanner +* Iteration Support via [Rows](#row--rows) +* sql.Scanner support * Every feature comes with tests * Convention Over Configuration * Developer Friendly @@ -695,6 +695,20 @@ for rows.Next() { } ``` +## Group & Having + +```go +rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Rows() +for rows.Next() { + ... +} + +rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Having("sum(amount) > ?", 100).Rows() +for rows.Next() { + ... +} +``` + ## Run Raw SQl ```go @@ -762,9 +776,7 @@ db.Where("email = ?", "x@example.org").Attrs(User{RegisteredIp: "111.111.111.111 ``` ## TODO -* Cache Stmt for performance -* Join, Having, Group, Includes -* Scopes, Valiations +* Scopes, Valiations, Includes, Joins, UpdateColumn/Columns * AlertColumn, DropColumn, AddIndex, RemoveIndex # Author diff --git a/do.go b/do.go index 143846c3..61e08406 100644 --- a/do.go +++ b/do.go @@ -342,11 +342,13 @@ func (s *Do) related(value interface{}, foreign_keys ...string) *Do { } func (s *Do) row() *sql.Row { + defer s.trace(time.Now()) s.prepareQuerySql() return s.db.db.QueryRow(s.sql, s.sqlVars...) } func (s *Do) rows() (*sql.Rows, error) { + defer s.trace(time.Now()) s.prepareQuerySql() return s.db.db.Query(s.sql, s.sqlVars...) } @@ -409,15 +411,12 @@ func (s *Do) query() *Do { } func (s *Do) count(value interface{}) *Do { - defer s.trace(time.Now()) s.search = s.search.clone().selects("count(*)") s.err(s.row().Scan(value)) return s } func (s *Do) pluck(column string, value interface{}) *Do { - defer s.trace(time.Now()) - dest_out := reflect.Indirect(reflect.ValueOf(value)) s.search = s.search.clone().selects(column) if dest_out.Kind() != reflect.Slice { @@ -634,8 +633,28 @@ func (s *Do) offsetSql() string { } } +func (s *Do) groupSql() string { + if len(s.search.groupStr) == 0 { + return "" + } else { + return " GROUP BY " + s.search.groupStr + } +} + +func (s *Do) havingSql() string { + if s.search.havingClause == nil { + return "" + } else { + return " HAVING " + s.buildWhereCondition(s.search.havingClause) + } +} + +func (s *Do) joinsSql() string { + return "" +} + func (s *Do) combinedSql() string { - return s.whereSql() + s.orderSql() + s.limitSql() + s.offsetSql() + return s.whereSql() + s.orderSql() + s.limitSql() + s.offsetSql() + s.groupSql() + s.havingSql() } func (s *Do) createTable() *Do { diff --git a/gorm_test.go b/gorm_test.go index 16ca7460..6dc6231a 100644 --- a/gorm_test.go +++ b/gorm_test.go @@ -1391,6 +1391,42 @@ func TestRows(t *testing.T) { } } +func TestGroup(t *testing.T) { + rows, err := db.Select("name").Table("users").Group("name").Rows() + + if err == nil { + defer rows.Close() + for rows.Next() { + var name string + rows.Scan(&name) + } + } else { + t.Errorf("Should not raise any error") + } +} + +func TestHaving(t *testing.T) { + rows, err := db.Debug().Select("name, count(*) as total").Table("users").Group("name").Having("name IN (?)", []string{"2", "3"}).Rows() + + if err == nil { + defer rows.Close() + for rows.Next() { + var name string + var total int64 + rows.Scan(&name, &total) + + if name == "2" && total != 1 { + t.Errorf("Should have one user having name 2", total) + } + if name == "3" && total != 2 { + t.Errorf("Should have two users having name 3", total) + } + } + } else { + t.Errorf("Should not raise any error", err) + } +} + func BenchmarkGorm(b *testing.B) { b.N = 2000 for x := 0; x < b.N; x++ { diff --git a/main.go b/main.go index 0648f6de..f1f0b985 100644 --- a/main.go +++ b/main.go @@ -77,6 +77,22 @@ func (s *DB) Select(value interface{}) *DB { return s.clone().search.selects(value).db } +func (s *DB) Group(query string) *DB { + return s.clone().search.group(query).db +} + +func (s *DB) Having(query string, values ...interface{}) *DB { + return s.clone().search.having(query, values...).db +} + +func (s *DB) Joins(query string) *DB { + return s.clone().search.joins(query).db +} + +func (s *DB) Includes(value interface{}) *DB { + return s.clone().search.includes(value).db +} + func (s *DB) Unscoped() *DB { return s.clone().search.unscoped().db } diff --git a/search.go b/search.go index 14103240..f3ff69ad 100644 --- a/search.go +++ b/search.go @@ -7,33 +7,39 @@ import ( ) type search struct { - db *DB - whereClause []map[string]interface{} - orClause []map[string]interface{} - notClause []map[string]interface{} - initAttrs []interface{} - assignAttrs []interface{} - orders []string - selectStr string - offsetStr string - limitStr string - tableName string - unscope bool + db *DB + whereClause []map[string]interface{} + orClause []map[string]interface{} + notClause []map[string]interface{} + initAttrs []interface{} + assignAttrs []interface{} + havingClause map[string]interface{} + orders []string + joinsStr string + selectStr string + offsetStr string + limitStr string + groupStr string + tableName string + unscope bool } func (s *search) clone() *search { return &search{ - whereClause: s.whereClause, - orClause: s.orClause, - notClause: s.notClause, - initAttrs: s.initAttrs, - assignAttrs: s.assignAttrs, - orders: s.orders, - selectStr: s.selectStr, - offsetStr: s.offsetStr, - limitStr: s.limitStr, - unscope: s.unscope, - tableName: s.tableName, + whereClause: s.whereClause, + orClause: s.orClause, + notClause: s.notClause, + initAttrs: s.initAttrs, + assignAttrs: s.assignAttrs, + havingClause: s.havingClause, + orders: s.orders, + selectStr: s.selectStr, + offsetStr: s.offsetStr, + limitStr: s.limitStr, + unscope: s.unscope, + groupStr: s.groupStr, + joinsStr: s.joinsStr, + tableName: s.tableName, } } @@ -86,6 +92,25 @@ func (s *search) offset(value interface{}) *search { return s } +func (s *search) group(query string) *search { + s.groupStr = s.getInterfaceAsSql(query) + return s +} + +func (s *search) having(query string, values ...interface{}) *search { + s.havingClause = map[string]interface{}{"query": query, "args": values} + return s +} + +func (s *search) includes(value interface{}) *search { + return s +} + +func (s *search) joins(query string) *search { + s.joinsStr = query + return s +} + func (s *search) unscoped() *search { s.unscope = true return s