Skip to content

Commit

Permalink
Added ForEach and Count methods
Browse files Browse the repository at this point in the history
Resolves issue #47
  • Loading branch information
timshannon committed Dec 28, 2020
1 parent 94bc303 commit 512d187
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 14 deletions.
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,23 @@ store.UpdateMatching(&Person{}, badgerhold.Where("Death").Lt(badgerhold.Field("B
})
```

If you simply want to count the number of records returned by a query use the `Count` method:

```Go
// need to pass in empty datatype so badgerhold knows what type to count
count, err := store.Count(&Person{}, badgerhold.Where("Death").Lt(badgerhold.Field("Birth")))
```

You can also use `FindOne` which is a shorthand for `Find` + `Limit(1)` which returns a single record instead of a slice
of records, and will return an `ErrNotFound` if no record is found, unlike a normal `Find` query where an empty slice
would be returned with no error.

```Go
result := &ItemTest{}
err := store.FindOne(result, query)
```


### Keys in Structs

A common scenario is to store the badgerhold Key in the same struct that is stored in the badgerDB value. You can automatically populate a record's Key in a struct by using the `badgerhold:"key"` struct tag when running `Find` queries.
Expand Down Expand Up @@ -176,6 +193,20 @@ type User struct {

The example above will only allow one record of type `User` to exist with a given `Email` field. Any insert, update or upsert that would violate that constraint will fail and return the `badgerhold.ErrUniqueExists` error.

### ForEach

When working with large datasets, you may not want to have to store the entire dataset in memory. It's be much more efficient to work with a single record at a time rather than grab all the records and loop through them, which is what cursors are used for in databases. In BadgerHold you can accomplish the same thing by calling ForEach:

```Go
err := store.ForEach(badgerhold.Where("Id").Gt(4), func(record *Item) error {
// do stuff with record

// if you return an error, then the query will stop iterating through records

return nil
})
```

### Aggregate Queries

Aggregate queries are queries that group results by a field. For example, lets say you had a collection of employees:
Expand Down
18 changes: 18 additions & 0 deletions find_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1157,3 +1157,21 @@ func TestFindOneWithNonPtr(t *testing.T) {
_ = store.FindOne(result, badgerhold.Where("Name").Eq("blah"))
})
}

func TestCount(t *testing.T) {
testWrap(t, func(store *badgerhold.Store, t *testing.T) {
insertTestData(t, store)
for _, tst := range testResults {
t.Run(tst.name, func(t *testing.T) {
count, err := store.Count(ItemTest{}, tst.query)
if err != nil {
t.Fatalf("Error counting data from badgerhold: %s", err)
}

if count != len(tst.result) {
t.Fatalf("Count result is %d wanted %d.", count, len(tst.result))
}
})
}
})
}
50 changes: 50 additions & 0 deletions foreach_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright 2019 Tim Shannon. All rights reserved.
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.

package badgerhold_test

import (
"fmt"
"testing"

"github.com/timshannon/badgerhold/v2"
)

func TestForEach(t *testing.T) {
testWrap(t, func(store *badgerhold.Store, t *testing.T) {
insertTestData(t, store)
for _, tst := range testResults {
t.Run(tst.name, func(t *testing.T) {
count := 0
err := store.ForEach(tst.query, func(record *ItemTest) error {
count++

found := false
for i := range tst.result {
if record.equal(&testData[tst.result[i]]) {
found = true
break
}
}

if !found {
if testing.Verbose() {
return fmt.Errorf("%v was not found in the result set! Full results: %v",
record, tst.result)
}
return fmt.Errorf("%v was not found in the result set!", record)
}

return nil
})
if count != len(tst.result) {
t.Fatalf("ForEach count is %d wanted %d.", count, len(tst.result))
}
if err != nil {
t.Fatalf("Error during ForEach iteration: %s", err)
}
})
}
})
}
43 changes: 29 additions & 14 deletions get.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,17 +91,32 @@ func (s *Store) TxFindOne(tx *badger.Txn, result interface{}, query *Query) erro
}

// Count returns the current record count for the passed in datatype
// func (s *Store) Count(dataType interface{}, query *Query) (int, error) {
// count := 0
// err := s.Bolt().View(func(tx *badger.Tx) error {
// var txErr error
// count, txErr = s.TxCount(tx, dataType, query)
// return txErr
// })
// return count, err
// }

// // TxCount returns the current record count from within the given transaction for the passed in datatype
// func (s *Store) TxCount(tx *badger.Tx, dataType interface{}, query *Query) (int, error) {
// return s.countQuery(tx, dataType, query)
// }
func (s *Store) Count(dataType interface{}, query *Query) (int, error) {
count := 0
err := s.Badger().View(func(tx *badger.Txn) error {
var txErr error
count, txErr = s.TxCount(tx, dataType, query)
return txErr
})
return count, err
}

// TxCount returns the current record count from within the given transaction for the passed in datatype
func (s *Store) TxCount(tx *badger.Txn, dataType interface{}, query *Query) (int, error) {
return countQuery(tx, dataType, query)
}

// ForEach runs the function fn against every record that matches the query
// Useful for when working with large sets of data that you don't want to hold the entire result
// set in memory, similar to database cursors
// Return an error from fn, will stop the cursor from iterating
func (s *Store) ForEach(query *Query, fn interface{}) error {
return s.Badger().View(func(tx *badger.Txn) error {
return s.TxForEach(tx, query, fn)
})
}

// TxForEach is the same as ForEach but you get to specify your transaction
func (s *Store) TxForEach(tx *badger.Txn, query *Query, fn interface{}) error {
return forEach(tx, query, fn)
}
48 changes: 48 additions & 0 deletions query.go
Original file line number Diff line number Diff line change
Expand Up @@ -1077,3 +1077,51 @@ func findOneQuery(tx *badger.Txn, result interface{}, query *Query) error {

return nil
}

func forEach(tx *badger.Txn, query *Query, fn interface{}) error {
if query == nil {
query = &Query{}
}

fnVal := reflect.ValueOf(fn)
argType := reflect.TypeOf(fn).In(0)

if argType.Kind() == reflect.Ptr {
argType = argType.Elem()
}

dataType := reflect.New(argType).Interface()

return runQuery(tx, dataType, query, nil, query.skip, func(r *record) error {
out := fnVal.Call([]reflect.Value{r.value})
if len(out) != 1 {
return fmt.Errorf("foreach function does not return an error")
}

if out[0].IsNil() {
return nil
}

return out[0].Interface().(error)
})
}

func countQuery(tx *badger.Txn, dataType interface{}, query *Query) (int, error) {
if query == nil {
query = &Query{}
}

count := 0

err := runQuery(tx, dataType, query, nil, query.skip,
func(r *record) error {
count++
return nil
})

if err != nil {
return 0, err
}

return count, nil
}

0 comments on commit 512d187

Please sign in to comment.