From c378a7a891614c22df51e491f4e8a7ad40b20ec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Nieto?= <jose.carlos@menteslibres.net> Date: Sun, 4 Oct 2015 16:33:02 -0500 Subject: [PATCH] Using query builder to replace db.Result. --- builder.go | 4 +- builder/builder.go | 315 ++++++++++++++++++++++++---------- postgresql/collection.go | 5 +- postgresql/database.go | 4 + postgresql/database_test.go | 60 +++++-- util/sqlgen/column_value.go | 8 + util/sqlutil/result/result.go | 277 +++++++----------------------- 7 files changed, 342 insertions(+), 331 deletions(-) diff --git a/builder.go b/builder.go index f67adfc5..a112e295 100644 --- a/builder.go +++ b/builder.go @@ -17,6 +17,7 @@ type QueryBuilder interface { } type QuerySelector interface { + Columns(columns ...interface{}) QuerySelector From(tables ...string) QuerySelector Distinct() QuerySelector Where(...interface{}) QuerySelector @@ -33,8 +34,9 @@ type QuerySelector interface { Limit(int) QuerySelector Offset(int) QuerySelector + Iterator() Iterator + QueryGetter - Iterator fmt.Stringer } diff --git a/builder/builder.go b/builder/builder.go index 568bd6b4..b594c3c1 100644 --- a/builder/builder.go +++ b/builder/builder.go @@ -5,6 +5,8 @@ import ( "errors" "fmt" "github.com/jmoiron/sqlx" + "github.com/jmoiron/sqlx/reflectx" + "reflect" "regexp" "strconv" "strings" @@ -15,8 +17,15 @@ import ( type SelectMode uint8 +var mapper = reflectx.NewMapper("db") + var ( - reInvisibleChars = regexp.MustCompile(`[\s\r\n\t]+`) + reInvisibleChars = regexp.MustCompile(`[\s\r\n\t]+`) + reColumnCompareExclude = regexp.MustCompile(`[^a-zA-Z0-9]`) +) + +var ( + sqlPlaceholder = sqlgen.RawValue(`?`) ) const ( @@ -28,6 +37,8 @@ type sqlDatabase interface { Query(stmt *sqlgen.Statement, args ...interface{}) (*sqlx.Rows, error) QueryRow(stmt *sqlgen.Statement, args ...interface{}) (*sqlx.Row, error) Exec(stmt *sqlgen.Statement, args ...interface{}) (sql.Result, error) + + TableColumns(tableName string) ([]string, error) } type Builder struct { @@ -46,16 +57,12 @@ func (b *Builder) SelectAllFrom(table string) db.QuerySelector { } func (b *Builder) Select(columns ...interface{}) db.QuerySelector { - f, err := columnFragments(b.t, columns) - qs := &QuerySelector{ builder: b, - columns: sqlgen.JoinColumns(f...), - err: err, } qs.stringer = &stringer{qs, b.t.Template} - return qs + return qs.Columns(columns...) } func (b *Builder) InsertInto(table string) db.QueryInserter { @@ -80,8 +87,9 @@ func (b *Builder) DeleteFrom(table string) db.QueryDeleter { func (b *Builder) Update(table string) db.QueryUpdater { qu := &QueryUpdater{ - builder: b, - table: table, + builder: b, + table: table, + columnValues: &sqlgen.ColumnValues{}, } qu.stringer = &stringer{qu, b.t.Template} @@ -195,9 +203,28 @@ type QueryUpdater struct { } func (qu *QueryUpdater) Set(terms ...interface{}) db.QueryUpdater { - cv, arguments := qu.builder.t.ToColumnValues(terms) - qu.columnValues = &cv - qu.arguments = append(qu.arguments, arguments...) + if len(terms) == 1 { + columns, _ := qu.builder.sess.TableColumns(qu.table) + ff, vv, _ := fieldValues(columns, terms[0]) + + cvs := make([]sqlgen.Fragment, len(ff)) + + for i := range ff { + cvs[i] = &sqlgen.ColumnValue{ + Column: sqlgen.ColumnWithName(ff[i]), + Operator: qu.builder.t.AssignmentOperator, + Value: sqlPlaceholder, + } + } + + qu.columnValues.Append(cvs...) + qu.arguments = append(qu.arguments, vv...) + } else if len(terms) > 1 { + cv, arguments := qu.builder.t.ToColumnValues(terms) + qu.columnValues.Append(cv.ColumnValues...) + qu.arguments = append(qu.arguments, arguments...) + } + return qu } @@ -235,10 +262,14 @@ func (qu *QueryUpdater) statement() *sqlgen.Statement { return stmt } +type iterator struct { + cursor *sqlx.Rows // This is the main query cursor. It starts as a nil value. + err error +} + type QuerySelector struct { *stringer mode SelectMode - cursor *sqlx.Rows // This is the main query cursor. It starts as a nil value. builder *Builder table string where *sqlgen.Where @@ -257,6 +288,16 @@ func (qs *QuerySelector) From(tables ...string) db.QuerySelector { return qs } +func (qs *QuerySelector) Columns(columns ...interface{}) db.QuerySelector { + f, err := columnFragments(qs.builder.t, columns) + if err != nil { + qs.err = err + return qs + } + qs.columns = sqlgen.JoinColumns(f...) + return qs +} + func (qs *QuerySelector) Distinct() db.QuerySelector { qs.mode = selectModeDistinct return qs @@ -426,88 +467,9 @@ func (qs *QuerySelector) QueryRow() (*sqlx.Row, error) { return qs.builder.sess.QueryRow(qs.statement(), qs.arguments...) } -func (qs *QuerySelector) Close() (err error) { - if qs.cursor != nil { - err = qs.cursor.Close() - qs.cursor = nil - } - return err -} - -func (qs *QuerySelector) setCursor() (err error) { - if qs.cursor == nil { - qs.cursor, err = qs.builder.sess.Query(qs.statement(), qs.arguments...) - } - return err -} - -func (qs *QuerySelector) One(dst interface{}) error { - if qs.err != nil { - return qs.err - } - - if qs.cursor != nil { - return db.ErrQueryIsPending - } - - defer qs.Close() - - if !qs.Next(dst) { - return qs.Err() - } - - return nil -} - -func (qs *QuerySelector) All(dst interface{}) error { - var err error - - if qs.err != nil { - return qs.err - } - - if qs.cursor != nil { - return db.ErrQueryIsPending - } - - err = qs.setCursor() - - if err != nil { - return err - } - - defer qs.Close() - - // Fetching all results within the cursor. - err = sqlutil.FetchRows(qs.cursor, dst) - - return err -} - -func (qs *QuerySelector) Err() (err error) { - return qs.err -} - -func (qs *QuerySelector) Next(dst interface{}) bool { - var err error - - if qs.err != nil { - return false - } - - if err = qs.setCursor(); err != nil { - qs.err = err - qs.Close() - return false - } - - if err = sqlutil.FetchRow(qs.cursor, dst); err != nil { - qs.err = err - qs.Close() - return false - } - - return true +func (qs *QuerySelector) Iterator() db.Iterator { + rows, err := qs.builder.sess.Query(qs.statement(), qs.arguments...) + return &iterator{rows, err} } func columnFragments(template *sqlutil.TemplateWithUtils, columns []interface{}) ([]sqlgen.Fragment, error) { @@ -585,3 +547,168 @@ func NewBuilder(sess sqlDatabase, t *sqlutil.TemplateWithUtils) *Builder { t: t, } } + +func (iter *iterator) One(dst interface{}) error { + if iter.err != nil { + return iter.err + } + + defer iter.Close() + + if !iter.Next(dst) { + return iter.Err() + } + + return nil +} + +func (iter *iterator) All(dst interface{}) error { + var err error + + if iter.err != nil { + return iter.err + } + + defer iter.Close() + + // Fetching all results within the cursor. + err = sqlutil.FetchRows(iter.cursor, dst) + + return err +} + +func (iter *iterator) Err() (err error) { + return iter.err +} + +func (iter *iterator) Next(dst interface{}) bool { + var err error + + if iter.err != nil { + return false + } + + if err = sqlutil.FetchRow(iter.cursor, dst); err != nil { + iter.err = err + iter.Close() + return false + } + + return true +} + +func (iter *iterator) Close() (err error) { + if iter.cursor != nil { + err = iter.cursor.Close() + iter.cursor = nil + } + return err +} + +func fieldValues(columns []string, item interface{}) ([]string, []interface{}, error) { + fields := []string{} + values := []interface{}{} + + itemV := reflect.ValueOf(item) + itemT := itemV.Type() + + if itemT.Kind() == reflect.Ptr { + // Single derefence. Just in case user passed a pointer to struct instead of a struct. + item = itemV.Elem().Interface() + itemV = reflect.ValueOf(item) + itemT = itemV.Type() + } + + switch itemT.Kind() { + + case reflect.Struct: + + fieldMap := mapper.TypeMap(itemT).Names + nfields := len(fieldMap) + + values = make([]interface{}, 0, nfields) + fields = make([]string, 0, nfields) + + for _, fi := range fieldMap { + // log.Println("=>", fi.Name, fi.Options) + + fld := reflectx.FieldByIndexesReadOnly(itemV, fi.Index) + if fld.Kind() == reflect.Ptr && fld.IsNil() { + continue + } + + var value interface{} + if _, ok := fi.Options["stringarray"]; ok { + value = sqlutil.StringArray(fld.Interface().([]string)) + } else if _, ok := fi.Options["int64array"]; ok { + value = sqlutil.Int64Array(fld.Interface().([]int64)) + } else if _, ok := fi.Options["jsonb"]; ok { + value = sqlutil.JsonbType{fld.Interface()} + } else { + value = fld.Interface() + } + + if _, ok := fi.Options["omitempty"]; ok { + if value == fi.Zero.Interface() { + continue + } + } + + // TODO: columnLike stuff...? + + fields = append(fields, fi.Name) + v, err := marshal(value) + if err != nil { + return nil, nil, err + } + values = append(values, v) + } + + case reflect.Map: + nfields := itemV.Len() + values = make([]interface{}, nfields) + fields = make([]string, nfields) + mkeys := itemV.MapKeys() + + for i, keyV := range mkeys { + valv := itemV.MapIndex(keyV) + fields[i] = columnLike(columns, fmt.Sprintf("%v", keyV.Interface())) + + v, err := marshal(valv.Interface()) + if err != nil { + return nil, nil, err + } + + values[i] = v + } + + default: + return nil, nil, db.ErrExpectingMapOrStruct + } + + return fields, values, nil +} + +func columnLike(columns []string, s string) string { + for _, name := range columns { + if normalizeColumn(s) == normalizeColumn(name) { + return name + } + } + return s +} + +func marshal(v interface{}) (interface{}, error) { + if m, isMarshaler := v.(db.Marshaler); isMarshaler { + var err error + if v, err = m.MarshalDB(); err != nil { + return nil, err + } + } + return v, nil +} + +// normalizeColumn prepares a column for comparison against another column. +func normalizeColumn(s string) string { + return strings.ToLower(reColumnCompareExclude.ReplaceAllString(s, "")) +} diff --git a/postgresql/collection.go b/postgresql/collection.go index 2d41d9dc..b1436045 100644 --- a/postgresql/collection.go +++ b/postgresql/collection.go @@ -41,9 +41,8 @@ type table struct { var _ = db.Collection(&table{}) // Find creates a result set with the given conditions. -func (t *table) Find(terms ...interface{}) db.Result { - where, arguments := template.ToWhereWithArguments(terms) - return result.NewResult(template, t, where, arguments) +func (t *table) Find(conds ...interface{}) db.Result { + return result.NewResult(t.database.Builder(), t, conds) } // Truncate deletes all rows from the table. diff --git a/postgresql/database.go b/postgresql/database.go index 90a2e45e..6487869e 100644 --- a/postgresql/database.go +++ b/postgresql/database.go @@ -528,6 +528,10 @@ func (d *database) tableExists(names ...string) error { return nil } +func (d *database) TableColumns(tableName string) ([]string, error) { + return d.tableColumns(tableName) +} + func (d *database) tableColumns(tableName string) ([]string, error) { // Making sure this table is allocated. diff --git a/postgresql/database_test.go b/postgresql/database_test.go index fc0b5bac..467eea64 100644 --- a/postgresql/database_test.go +++ b/postgresql/database_test.go @@ -2083,6 +2083,25 @@ func TestQueryBuilder(t *testing.T) { b.Update("artist").Set("name = ?", "Artist").Where("id <", 5).String(), ) + assert.Equal( + `UPDATE "artist" SET "name" = $1 WHERE ("id" < $2)`, + b.Update("artist").Set(map[string]string{"name": "Artist"}).Where(db.Cond{"id <": 5}).String(), + ) + + assert.Equal( + `UPDATE "artist" SET "name" = $1 WHERE ("id" < $2)`, + b.Update("artist").Set(struct { + Nombre string `db:"name"` + }{"Artist"}).Where(db.Cond{"id <": 5}).String(), + ) + + assert.Equal( + `UPDATE "artist" SET "name" = $1, "last_name" = $2 WHERE ("id" < $3)`, + b.Update("artist").Set(struct { + Nombre string `db:"name"` + }{"Artist"}).Set(map[string]string{"last_name": "Foo"}).Where(db.Cond{"id <": 5}).String(), + ) + assert.Equal( `UPDATE "artist" SET "name" = $1 || ' ' || $2 || id, "id" = id + $3 WHERE (id > $4)`, b.Update("artist").Set( @@ -2114,32 +2133,35 @@ func TestQueryBuilder(t *testing.T) { // Testing actual queries. - var artist artistType - var artists []artistType + /* + var artist artistType + var artists []artistType - err = b.SelectAllFrom("artist").All(&artists) - assert.NoError(err) - assert.True(len(artists) > 0) + err = b.SelectAllFrom("artist").Iterator().All(&artists) + assert.NoError(err) + assert.True(len(artists) > 0) - err = b.SelectAllFrom("artist").One(&artist) - assert.NoError(err) - assert.NotNil(artist) + err = b.SelectAllFrom("artist").Iterator().One(&artist) + assert.NoError(err) + assert.NotNil(artist) - var qs db.QuerySelector + var qs db.QuerySelector - qs = b.SelectAllFrom("artist") - for qs.Next(&artist) { - assert.Nil(qs.Err()) - assert.NotNil(artist) - } + qs = b.SelectAllFrom("artist") + iter := qs.Iterator() + for iter.Next(&artist) { + assert.Nil(iter.Err()) + assert.NotNil(artist) + } - assert.Nil(qs.Close()) + assert.Nil(iter.Close()) - qs = b.Select().From("artist a").Join("publications p").On("p1.id = a.id").Using("id") - assert.Error(qs.One(&artist), `Should not work because it attempts to use both "On()" and "Using()" in the same JOIN.`) + qs = b.Select().From("artist a").Join("publications p").On("p1.id = a.id").Using("id") + assert.Error(qs.Iterator().One(&artist), `Should not work because it attempts to use both "On()" and "Using()" in the same JOIN.`) - qs = b.Select().From("artist a").On("p1.id = a.id") - assert.Error(qs.One(&artist), `Should not work because it should put a "Join()" before "On()".`) + qs = b.Select().From("artist a").On("p1.id = a.id") + assert.Error(qs.Iterator().One(&artist), `Should not work because it should put a "Join()" before "On()".`) + */ } // TestExhaustConnections simulates a "too many connections" situation diff --git a/util/sqlgen/column_value.go b/util/sqlgen/column_value.go index 485a46f3..9674b41a 100644 --- a/util/sqlgen/column_value.go +++ b/util/sqlgen/column_value.go @@ -58,6 +58,14 @@ func JoinColumnValues(values ...Fragment) *ColumnValues { return &ColumnValues{ColumnValues: values} } +func (c *ColumnValues) Append(values ...Fragment) *ColumnValues { + for _, f := range values { + c.ColumnValues = append(c.ColumnValues, f) + } + c.hash = "" + return c +} + // Hash returns a unique identifier. func (c *ColumnValues) Hash() string { if c.hash == "" { diff --git a/util/sqlutil/result/result.go b/util/sqlutil/result/result.go index c619ec5d..cc2345d0 100644 --- a/util/sqlutil/result/result.go +++ b/util/sqlutil/result/result.go @@ -22,104 +22,55 @@ package result import ( - "fmt" - "strings" - - "github.com/jmoiron/sqlx" "upper.io/db" - "upper.io/db/util/sqlgen" - "upper.io/db/util/sqlutil" -) - -var ( - sqlPlaceholder = sqlgen.RawValue(`?`) ) -type counter struct { - Total uint64 `db:"_t"` -} - type Result struct { - table DataProvider - cursor *sqlx.Rows // This is the main query cursor. It starts as a nil value. - limit sqlgen.Limit - offset sqlgen.Offset - columns sqlgen.Columns - where sqlgen.Where - orderBy sqlgen.OrderBy - groupBy sqlgen.GroupBy - arguments []interface{} - template *sqlutil.TemplateWithUtils + b db.QueryBuilder + dp DataProvider + iter db.Iterator + limit int + offset int + fields []interface{} + columns []interface{} + orderBy []interface{} + groupBy []interface{} + conds []interface{} } // NewResult creates and results a new result set on the given table, this set // is limited by the given sqlgen.Where conditions. -func NewResult(template *sqlutil.TemplateWithUtils, p DataProvider, where sqlgen.Where, arguments []interface{}) *Result { +func NewResult(b db.QueryBuilder, dp DataProvider, conds []interface{}) *Result { return &Result{ - table: p, - where: where, - arguments: arguments, - template: template, - } -} - -// Executes a SELECT statement that can feed Next(), All() or One(). -func (r *Result) setCursor() error { - var err error - // We need a cursor, if the cursor does not exists yet then we create one. - if r.cursor == nil { - stmt := sqlgen.Statement{ - Type: sqlgen.Select, - Table: sqlgen.TableWithName(r.table.Name()), - Columns: &r.columns, - Limit: r.limit, - Offset: r.offset, - Where: &r.where, - OrderBy: &r.orderBy, - GroupBy: &r.groupBy, - } - r.cursor, err = r.table.Query(&stmt, r.arguments...) + b: b, + dp: dp, + conds: conds, } - return err } // Sets conditions for reducing the working set. -func (r *Result) Where(terms ...interface{}) db.Result { - w, a := r.template.ToWhereWithArguments(terms) - r.where = w - r.arguments = append(r.arguments, a...) +func (r *Result) Where(conds ...interface{}) db.Result { + r.conds = conds return r } // Determines the maximum limit of results to be returned. func (r *Result) Limit(n uint) db.Result { - r.limit = sqlgen.Limit(n) + r.limit = int(n) return r } // Determines how many documents will be skipped before starting to grab // results. func (r *Result) Skip(n uint) db.Result { - r.offset = sqlgen.Offset(n) + r.offset = int(n) return r } // Used to group results that have the same value in the same column or // columns. func (r *Result) Group(fields ...interface{}) db.Result { - var columns []sqlgen.Fragment - - for i := range fields { - switch v := fields[i].(type) { - case string: - columns = append(columns, sqlgen.ColumnWithName(v)) - case sqlgen.Fragment: - columns = append(columns, v) - } - } - - r.groupBy = *sqlgen.GroupByColumns(columns...) - + r.groupBy = fields return r } @@ -127,195 +78,93 @@ func (r *Result) Group(fields ...interface{}) db.Result { // prefixed by - (minus) which means descending order, ascending order would be // used otherwise. func (r *Result) Sort(fields ...interface{}) db.Result { - - var sortColumns sqlgen.SortColumns - - for i := range fields { - var sort *sqlgen.SortColumn - - switch value := fields[i].(type) { - case db.Raw: - sort = &sqlgen.SortColumn{ - Column: sqlgen.RawValue(fmt.Sprintf(`%v`, value.Value)), - Order: sqlgen.Ascendent, - } - case string: - if strings.HasPrefix(value, `-`) { - // Explicit descending order. - sort = &sqlgen.SortColumn{ - Column: sqlgen.ColumnWithName(value[1:]), - Order: sqlgen.Descendent, - } - } else { - // Ascending order. - sort = &sqlgen.SortColumn{ - Column: sqlgen.ColumnWithName(value), - Order: sqlgen.Ascendent, - } - } - } - sortColumns.Columns = append(sortColumns.Columns, sort) - } - - r.orderBy.SortColumns = &sortColumns - + r.orderBy = fields return r } // Retrieves only the given fields. func (r *Result) Select(fields ...interface{}) db.Result { - - r.columns = sqlgen.Columns{} - - for i := range fields { - var col sqlgen.Fragment - switch value := fields[i].(type) { - case db.Func: - v := r.template.ToInterfaceArguments(value.Args) - var s string - if len(v) == 0 { - s = fmt.Sprintf(`%s()`, value.Name) - } else { - ss := make([]string, 0, len(v)) - for j := range v { - ss = append(ss, fmt.Sprintf(`%v`, v[j])) - } - s = fmt.Sprintf(`%s(%s)`, value.Name, strings.Join(ss, `, `)) - } - col = sqlgen.RawValue(s) - case db.Raw: - col = sqlgen.RawValue(fmt.Sprintf(`%v`, value.Value)) - default: - col = sqlgen.ColumnWithName(fmt.Sprintf(`%v`, value)) - } - r.columns.Columns = append(r.columns.Columns, col) - } - + r.fields = fields return r } // Dumps all results into a pointer to an slice of structs or maps. func (r *Result) All(dst interface{}) error { - var err error - - if r.cursor != nil { - return db.ErrQueryIsPending - } - - // Current cursor. - err = r.setCursor() - - if err != nil { - return err - } - - defer r.Close() - - // Fetching all results within the cursor. - err = sqlutil.FetchRows(r.cursor, dst) - - return err + return r.buildSelect().Iterator().All(dst) } // Fetches only one result from the resultset. func (r *Result) One(dst interface{}) error { - var err error - - if r.cursor != nil { - return db.ErrQueryIsPending - } - - defer r.Close() - - err = r.Next(dst) - - return err + return r.buildSelect().Iterator().One(dst) } // Fetches the next result from the resultset. func (r *Result) Next(dst interface{}) (err error) { - - if err = r.setCursor(); err != nil { - r.Close() - return err + if r.iter == nil { + r.iter = r.buildSelect().Iterator() } - - if err = sqlutil.FetchRow(r.cursor, dst); err != nil { - r.Close() - return err + if !r.iter.Next(dst) { + return r.iter.Err() } - return nil } // Removes the matching items from the collection. func (r *Result) Remove() error { - var err error - - _, err = r.table.Exec(&sqlgen.Statement{ - Type: sqlgen.Delete, - Table: sqlgen.TableWithName(r.table.Name()), - Where: &r.where, - }, r.arguments...) + q := r.b.DeleteFrom(r.dp.Name()). + Where(r.conds...). + Limit(r.limit) + _, err := q.Exec() return err +} +// Closes the result set. +func (r *Result) Close() error { + if r.iter != nil { + return r.iter.Close() + } + return nil } // Updates matching items from the collection with values of the given map or // struct. func (r *Result) Update(values interface{}) error { + q := r.b.Update(r.dp.Name()). + Set(values). + Where(r.conds...). + Limit(r.limit) - ff, vv, err := r.table.FieldValues(values) - if err != nil { - return err - } - - cvs := new(sqlgen.ColumnValues) - - for i := range ff { - cvs.ColumnValues = append(cvs.ColumnValues, &sqlgen.ColumnValue{Column: sqlgen.ColumnWithName(ff[i]), Operator: r.template.AssignmentOperator, Value: sqlPlaceholder}) - } - - vv = append(vv, r.arguments...) - - _, err = r.table.Exec(&sqlgen.Statement{ - Type: sqlgen.Update, - Table: sqlgen.TableWithName(r.table.Name()), - ColumnValues: cvs, - Where: &r.where, - }, vv...) - - return err -} - -// Closes the result set. -func (r *Result) Close() (err error) { - if r.cursor != nil { - err = r.cursor.Close() - r.cursor = nil - } + _, err := q.Exec() return err } // Counts the elements within the main conditions of the set. func (r *Result) Count() (uint64, error) { - var count counter + counter := struct { + Count uint64 `db:"_t"` + }{} - row, err := r.table.QueryRow(&sqlgen.Statement{ - Type: sqlgen.Count, - Table: sqlgen.TableWithName(r.table.Name()), - Where: &r.where, - }, r.arguments...) + q := r.buildSelect() + q.Columns(db.Raw{"COUNT(1) AS _t"}).Limit(1) - if err != nil { + if err := q.Iterator().One(&counter); err != nil { return 0, err } - err = row.Scan(&count.Total) - if err != nil { - return 0, err - } + return counter.Count, nil +} + +func (r *Result) buildSelect() db.QuerySelector { + q := r.b.Select(r.fields...) + + q.From(r.dp.Name()) + q.Where(r.conds...) + q.Limit(r.limit) + q.Offset(r.offset) + + q.GroupBy(r.groupBy...) + q.OrderBy(r.orderBy...) - return count.Total, nil + return q } -- GitLab