Skip to content

Commit

Permalink
* #102: Change Chunk method to accept struct and returning slice of t…
Browse files Browse the repository at this point in the history
…hose structs

Add common abstract method with EachToStruct processing data
Add more tests to cover errs

Add common abstract method with EachToStruct processing data
Add more tests to cover errs

- Remove outdated Get method
  • Loading branch information
arthurkushman committed Oct 21, 2023
1 parent 52d5874 commit 98659a4
Show file tree
Hide file tree
Showing 7 changed files with 619 additions and 257 deletions.
38 changes: 32 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ library [![Tweet](http://jpillora.com/github-twitter-button/img/tweet.png)](http
* [Create table](#user-content-create-table)
* [Add / Modify / Drop columns](#user-content-add--modify--drop-columns)
* [Chunking Results](#user-content-chunking-results)
* [Pluck / PluckMap](#user-content-pluck--pluckmap)

## Installation

Expand Down Expand Up @@ -433,17 +434,42 @@ If you need to work with thousands of database records, consider using the chunk
This method retrieves a small chunk of the results at a time and feeds each chunk into a closure for processing.

```go
err = db.Table("user_achievements").Select("points").Where("id", "=", id).Chunk(100, func (users []map[string]interface{}) bool {
for _, m := range users {
if val, ok := m["points"];ok {
pointsCalc += diffFormula(val.(int64))
}
// or you can return false here to stop running chunks
var sumOfPoints int64
dataStruct := &DataStructUser{}
err = db.Table(UsersTable).Select("name", "points").Chunk(dataStruct, 100, func(users []any) bool {
for _, v := range users {
user := v.(DataStructUser)
// your code goes here e.g.:
sumOfPoints += user.Points
}

// or you can return false here to stop running chunks
return true
})
```

## Pluck / PluckMap

If you would like to get values of a particular column(s) of a struct and place them into slice - use `Pluck` method:
```go
dataStruct := &DataStructUser{}
res, err := db.Table(UsersTable).Pluck(dataStruct)
for k, v := range res {
val := v.(DataStructUser)
fmt.Println(val.Name) // f.e.: Alex Shmidt
}

// or use a PluckMap method to aggregate key/value pairs to a map
res, err := db.Table(UsersTable).PluckMap(dataStruct, "name", "points")
for k, m := range res {
for key, value := range m {
keyVal := key.(string)
valueVal := value.(DataStructUser)
// rest of the code ...
}
}
```

PS Why use buildsqlx? Because it is simple and fast, yet versatile. The builder code-style has been inherited from greatest web-frameworks, so u can easily query anything from db.

Supporters gratitude:
Expand Down
83 changes: 58 additions & 25 deletions advanced.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package buildsqlx

import (
"database/sql"
"fmt"
"math"
"reflect"
"strconv"
)

Expand Down Expand Up @@ -31,44 +33,50 @@ func (r *DB) Find(src any, id uint64) error {
return r.Where("id", "=", id).First(src)
}

// Pluck getting values of a particular column and place them into slice
func (r *DB) Pluck(column string) (val []interface{}, err error) {
res, err := r.Get()
// Pluck getting values of a particular column(s) of a struct and place them into slice
func (r *DB) Pluck(src any) ([]any, error) {
res, err := r.eachToStructRows(src, r.Builder.offset, r.Builder.limit)
if err != nil {
return nil, err
}

val = make([]interface{}, len(res))
for k, m := range res {
val[k] = m[column]
}
return
return res, nil
}

// PluckMap getting values of a particular key/value columns and place them into map
func (r *DB) PluckMap(colKey, colValue string) (val []map[interface{}]interface{}, err error) {
res, err := r.Get()
// values of the returning map is a structure passed as src and filled with data from DB
func (r *DB) PluckMap(src any, colKey, colValue string) (val []map[any]any, err error) {
resource := reflect.ValueOf(src).Elem()
if err = validateFields(resource, src, []string{colKey, colValue}); err != nil {
return nil, err
}

res, err := r.eachToStructRows(src, r.Builder.offset, r.Builder.limit)
if err != nil {
return nil, err
}

val = make([]map[interface{}]interface{}, len(res))
val = make([]map[any]any, len(res))
for k, m := range res {
val[k] = make(map[interface{}]interface{})
val[k][m[colKey]] = m[colValue]
val[k] = make(map[any]any)

fieldKeyData := getFieldValue(m, colKey)
val[k][fieldKeyData] = reflect.ValueOf(m).Interface()
}

return
}

// Exists checks whether conditional rows are existing (returns true) or not (returns false)
func (r *DB) Exists() (exists bool, err error) {
builder := r.Builder
if builder.table == "" {
bldr := r.Builder
if bldr.table == "" {
return false, errTableCallBeforeOp
}

query := `SELECT EXISTS(SELECT 1 FROM "` + builder.table + `" ` + builder.buildClauses() + `)`
query := `SELECT EXISTS(SELECT 1 FROM "` + bldr.table + `" ` + bldr.buildClauses() + `)`
err = r.Sql().QueryRow(query, prepareValues(r.Builder.whereBindings)...).Scan(&exists)

return
}

Expand All @@ -78,6 +86,7 @@ func (r *DB) DoesntExists() (bool, error) {
if err != nil {
return false, err
}

return !ex, nil
}

Expand All @@ -93,8 +102,8 @@ func (r *DB) Decrement(column string, on uint64) (int64, error) {

// increments or decrements depending on sign
func (r *DB) incrDecr(column, sign string, on uint64) (int64, error) {
builder := r.Builder
if builder.table == "" {
bldr := r.Builder
if bldr.table == "" {
return 0, errTableCallBeforeOp
}

Expand All @@ -110,7 +119,7 @@ func (r *DB) incrDecr(column, sign string, on uint64) (int64, error) {

// Chunk run queries by chinks by passing user-land function with an ability to stop execution when needed
// by returning false and proceed to execute queries when return true
func (r *DB) Chunk(amount int64, fn func(rows []map[string]interface{}) bool) error {
func (r *DB) Chunk(src any, amount int64, fn func(rows []any) bool) error {
cols := r.Builder.columns
cnt, err := r.Count()
if err != nil {
Expand All @@ -123,26 +132,50 @@ func (r *DB) Chunk(amount int64, fn func(rows []map[string]interface{}) bool) er
}

if cnt < amount {
res, err := r.Get()
structRows, err := r.eachToStructRows(src, 0, 0)
if err != nil {
return err
}
fn(res) // execute all resulting records

fn(structRows) // execute all resulting records

return nil
}

// executing chunks amount < cnt
c := int64(math.Ceil(float64(cnt / amount)))
var i int64
for i = 0; i < c; i++ {
rows, err := r.Offset(i * amount).Limit(amount).Get() // by 100 rows from 100 x n
for i := int64(0); i < c; i++ {
structRows, err := r.eachToStructRows(src, i*amount, amount)
if err != nil {
return err
}
res := fn(rows)

res := fn(structRows)
if !res { // stop an execution when false returned by user
break
}
}

return nil
}

func (r *DB) eachToStructRows(src any, offset, limit int64) ([]any, error) {
var structRows []any
if limit > 0 {
r.Offset(offset).Limit(limit)
}

err := r.EachToStruct(func(rows *sql.Rows) error {
err := r.Next(rows, src)
if err != nil {
return err
}

v := reflect.ValueOf(src).Elem().Interface()
structRows = append(structRows, v)

return nil
})

return structRows, err
}
39 changes: 22 additions & 17 deletions aggregates.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,50 @@ package buildsqlx

// Count counts resulting rows based on clause
func (r *DB) Count() (cnt int64, err error) {
builder := r.Builder
builder.columns = []string{"COUNT(*)"}
query := builder.buildSelect()
bldr := r.Builder
bldr.columns = []string{"COUNT(*)"}
query := bldr.buildSelect()
err = r.Sql().QueryRow(query, prepareValues(r.Builder.whereBindings)...).Scan(&cnt)

return
}

// Avg calculates average for specified column
func (r *DB) Avg(column string) (avg float64, err error) {
builder := r.Builder
builder.columns = []string{"AVG(" + column + ")"}
query := builder.buildSelect()
bldr := r.Builder
bldr.columns = []string{"AVG(" + column + ")"}
query := bldr.buildSelect()
err = r.Sql().QueryRow(query, prepareValues(r.Builder.whereBindings)...).Scan(&avg)

return
}

// Min calculates minimum for specified column
func (r *DB) Min(column string) (min float64, err error) {
builder := r.Builder
builder.columns = []string{"MIN(" + column + ")"}
query := builder.buildSelect()
bldr := r.Builder
bldr.columns = []string{"MIN(" + column + ")"}
query := bldr.buildSelect()
err = r.Sql().QueryRow(query, prepareValues(r.Builder.whereBindings)...).Scan(&min)

return
}

// Max calculates maximum for specified column
func (r *DB) Max(column string) (max float64, err error) {
builder := r.Builder
builder.columns = []string{"MAX(" + column + ")"}
query := builder.buildSelect()
bldr := r.Builder
bldr.columns = []string{"MAX(" + column + ")"}
query := bldr.buildSelect()
err = r.Sql().QueryRow(query, prepareValues(r.Builder.whereBindings)...).Scan(&max)

return
}

// Sum calculates sum for specified column
func (r *DB) Sum(column string) (max float64, err error) {
builder := r.Builder
builder.columns = []string{"SUM(" + column + ")"}
query := builder.buildSelect()
err = r.Sql().QueryRow(query, prepareValues(r.Builder.whereBindings)...).Scan(&max)
func (r *DB) Sum(column string) (sum float64, err error) {
bldr := r.Builder
bldr.columns = []string{"SUM(" + column + ")"}
query := bldr.buildSelect()
err = r.Sql().QueryRow(query, prepareValues(r.Builder.whereBindings)...).Scan(&sum)

return
}
40 changes: 13 additions & 27 deletions builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -371,9 +371,10 @@ func (r *DB) Rename(from, to string) (sql.Result, error) {
// WhereIn appends IN (val1, val2, val3...) stmt to WHERE clause
func (r *DB) WhereIn(field string, in any) *DB {
ins, err := interfaceToSlice(in)
if err != nil {
return nil
if err != nil { // don't want the code run on prod falling just because user didn't pass slice as `in` param
log.Panicln(err)
}

r.buildWhere("", field, "IN", ins)
return r
}
Expand All @@ -382,8 +383,9 @@ func (r *DB) WhereIn(field string, in any) *DB {
func (r *DB) WhereNotIn(field string, in any) *DB {
ins, err := interfaceToSlice(in)
if err != nil {
return nil
log.Panicln(err)
}

r.buildWhere("", field, "NOT IN", ins)
return r
}
Expand All @@ -392,8 +394,9 @@ func (r *DB) WhereNotIn(field string, in any) *DB {
func (r *DB) OrWhereIn(field string, in any) *DB {
ins, err := interfaceToSlice(in)
if err != nil {
return nil
log.Panicln(err)
}

r.buildWhere("OR", field, "IN", ins)
return r
}
Expand All @@ -402,8 +405,9 @@ func (r *DB) OrWhereIn(field string, in any) *DB {
func (r *DB) OrWhereNotIn(field string, in any) *DB {
ins, err := interfaceToSlice(in)
if err != nil {
return nil
log.Panicln(err)
}

r.buildWhere("OR", field, "NOT IN", ins)
return r
}
Expand All @@ -412,8 +416,9 @@ func (r *DB) OrWhereNotIn(field string, in any) *DB {
func (r *DB) AndWhereIn(field string, in any) *DB {
ins, err := interfaceToSlice(in)
if err != nil {
return nil
log.Panicln(err)
}

r.buildWhere("AND", field, "IN", ins)
// r.buildWhere("AND", field, "IN", prepareSlice(ins))
return r
Expand All @@ -423,8 +428,9 @@ func (r *DB) AndWhereIn(field string, in any) *DB {
func (r *DB) AndWhereNotIn(field string, in any) *DB {
ins, err := interfaceToSlice(in)
if err != nil {
return nil
log.Panicln(err)
}

r.buildWhere("AND", field, "NOT IN", ins)
return r
}
Expand Down Expand Up @@ -459,26 +465,6 @@ func (r *DB) AndWhereNotNull(field string) *DB {
return r.buildWhere(sqlOperatorAnd, field, sqlOperatorIs, sqlSpecificValueNotNull)
}

// prepares slice for Where bindings, IN/NOT IN etc
func prepareSlice(in []any) (out []string) {
for _, value := range in {
switch v := value.(type) {
case string:
out = append(out, v)
case int:
out = append(out, strconv.FormatInt(int64(v), 10))
case float64:
out = append(out, fmt.Sprintf("%g", v))
case int64:
out = append(out, strconv.FormatInt(v, 10))
case uint64:
out = append(out, strconv.FormatUint(v, 10))
}
}

return
}

// From prepares sql stmt to set data from another table, ex.:
// UPDATE employees SET sales_count = sales_count + 1 FROM accounts
func (r *DB) From(fromTbl string) *DB {
Expand Down
Loading

0 comments on commit 98659a4

Please sign in to comment.