Skip to content

Commit

Permalink
Merge pull request #28 from timshannon/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
timshannon authored Sep 29, 2017
2 parents bc8956a + d79ac27 commit bb7fe75
Show file tree
Hide file tree
Showing 6 changed files with 245 additions and 176 deletions.
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ Optionally, you can implement the `Storer` interface, to specify your own indexe
struct tag.

## Queries
Queries are chain-able constructs that filters out any data that doesn't match it's criteria. There will be no
"query optimiser". The first field listed in the query will be the index that the query starts at (if one exists).
Queries are chain-able constructs that filters out any data that doesn't match it's criteria. An index will be used if
the `.Index()` chain is called, otherwise bolthold won't use any index.

Queries will look like this:
```Go
Expand All @@ -64,6 +64,7 @@ Fields must be exported, and thus always need to start with an upper-case letter
* Limit - `Where("field").Eq(value).Limit(10)`
* SortBy - `Where("field").Eq(value).SortBy("field1", "field2")`
* Reverse - `Where("field").Eq(value).SortBy("field").Reverse()`
* Index - `Where("field").Eq(value).Index("indexName")`


If you want to run a query's criteria against the Key value, you can use the `bolthold.Key` constant:
Expand Down Expand Up @@ -236,7 +237,7 @@ err = store.Insert("key", &Item{

That's it!

Bolthold is still very much alpha software at this point, but there is currently over 80% coverage in unit tests, and
it's backed by BoltDB which is a very solid and well built piece of software, so I encourage you to give it a try.
Bolthold currently has over 80% coverage in unit tests, and it's backed by BoltDB which is a very solid and well built
piece of software, so I encourage you to give it a try.

If you end up using BoltHold, I'd love to hear about it.
44 changes: 44 additions & 0 deletions aggregate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -475,3 +475,47 @@ func TestFindAggregateBadSumFieldPanic(t *testing.T) {
result[0].Sum("BadField")
})
}

func TestFindAggregateBadGroupField(t *testing.T) {
testWrap(t, func(store *bolthold.Store, t *testing.T) {
insertTestData(t, store)

_, err := store.FindAggregate(&ItemTest{}, nil, "BadField")
if err == nil {
t.Fatalf("FindAggregate didn't fail when grouped by a bad field.")
}

})
}

func TestFindAggregateWithNoResult(t *testing.T) {
testWrap(t, func(store *bolthold.Store, t *testing.T) {
insertTestData(t, store)

result, err := store.FindAggregate(&ItemTest{}, bolthold.Where("Name").Eq("Never going to match on this"), "Category")
if err != nil {
t.Fatalf("FindAggregate failed when the query produced no results")
}

if len(result) != 0 {
t.Fatalf("Incorrect result. Wanted 0 got %d", len(result))
}

})
}

func TestFindAggregateWithNoGroupBy(t *testing.T) {
testWrap(t, func(store *bolthold.Store, t *testing.T) {
insertTestData(t, store)

result, err := store.FindAggregate(&ItemTest{}, nil)
if err != nil {
t.Fatalf("FindAggregate failed when there was no groupBy ")
}

if len(result) != 1 {
t.Fatalf("Incorrect result. Wanted 1 got %d", len(result))
}

})
}
6 changes: 3 additions & 3 deletions compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ type Comparer interface {
Compare(other interface{}) (int, error)
}

func (c *Criterion) compare(rowValue, criterionValue interface{}) (int, error) {
func (c *Criterion) compare(rowValue, criterionValue interface{}, currentRow interface{}) (int, error) {
if rowValue == nil || criterionValue == nil {
if rowValue == criterionValue {
return 0, nil
Expand All @@ -40,10 +40,10 @@ func (c *Criterion) compare(rowValue, criterionValue interface{}) (int, error) {
}

if _, ok := criterionValue.(Field); ok {
fVal := reflect.ValueOf(c.query.currentRow).Elem().FieldByName(string(criterionValue.(Field)))
fVal := reflect.ValueOf(currentRow).Elem().FieldByName(string(criterionValue.(Field)))
if !fVal.IsValid() {
return 0, fmt.Errorf("The field %s does not exist in the type %s", criterionValue,
reflect.TypeOf(c.query.currentRow))
reflect.TypeOf(currentRow))
}

criterionValue = fVal.Interface()
Expand Down
72 changes: 53 additions & 19 deletions find_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,16 @@ var tests = []test{
query: bolthold.Where("ID").In(5, 8, 3),
result: []int{6, 7, 4, 13, 3},
},
test{
name: "In on data from other index",
query: bolthold.Where("ID").In(5, 8, 3).Index("Category"),
result: []int{6, 7, 4, 13, 3},
},
test{
name: "In on index",
query: bolthold.Where("Category").In("food", "animal").Index("Category"),
result: []int{2, 4, 5, 7, 8, 9, 10, 12, 13, 14, 15, 16},
},
test{
name: "Regular Expression",
query: bolthold.Where("Name").RegExp(regexp.MustCompile("ea")),
Expand Down Expand Up @@ -432,6 +442,19 @@ var tests = []test{
}),
result: []int{2, 4, 5, 7, 8, 9, 10, 12, 13, 14, 15, 16},
},
test{
name: "Issue #8 - Function Field on a specific index",
query: bolthold.Where("Category").MatchFunc(func(ra *bolthold.RecordAccess) (bool, error) {
field := ra.Field()
_, ok := field.(string)
if !ok {
return false, fmt.Errorf("Field not a string, it's a %T!", field)
}

return !strings.HasPrefix(field.(string), "veh"), nil
}).Index("Category"),
result: []int{2, 4, 5, 7, 8, 9, 10, 12, 13, 14, 15, 16},
},
test{
name: "Find item with max ID in each category - sub aggregate query",
query: bolthold.Where("ID").MatchFunc(func(ra *bolthold.RecordAccess) (bool, error) {
Expand All @@ -453,6 +476,21 @@ var tests = []test{
query: bolthold.Where("Category").In("animal", "vehicle"),
result: []int{0, 1, 2, 3, 5, 6, 8, 9, 11, 13, 14, 16},
},
test{
name: "Equal Field With Specific Index",
query: bolthold.Where("Category").Eq("vehicle").Index("Category"),
result: []int{0, 1, 3, 6, 11},
},
test{
name: "Key test after lead field",
query: bolthold.Where("Category").Eq("food").And(bolthold.Key).Gt(testData[10].Key),
result: []int{12, 15},
},
test{
name: "Key test after lead index",
query: bolthold.Where("Category").Eq("food").Index("Category").And(bolthold.Key).Gt(testData[10].Key),
result: []int{12, 15},
},
}

func insertTestData(t *testing.T, store *bolthold.Store) {
Expand Down Expand Up @@ -582,11 +620,24 @@ func TestFindOnInvalidFieldName(t *testing.T) {
})
}

func TestFindOnInvalidIndex(t *testing.T) {
testWrap(t, func(store *bolthold.Store, t *testing.T) {
insertTestData(t, store)
var result []ItemTest

err := store.Find(&result, bolthold.Where("Name").Eq("test").Index("BadIndex"))
if err == nil {
t.Fatalf("Find query against a bad index name didn't return an error!")
}

})
}

func TestQueryStringPrint(t *testing.T) {
q := bolthold.Where("FirstField").Eq("first value").And("SecondField").Gt("Second Value").And("ThirdField").
Lt("Third Value").And("FourthField").Ge("FourthValue").And("FifthField").Le("FifthValue").And("SixthField").
Ne("Sixth Value").Or(bolthold.Where("FirstField").In("val1", "val2", "val3").And("SecondField").IsNil().
And("ThirdField").RegExp(regexp.MustCompile("test")).And("FirstField").
And("ThirdField").RegExp(regexp.MustCompile("test")).Index("IndexName").And("FirstField").
MatchFunc(func(ra *bolthold.RecordAccess) (bool, error) {
return true, nil
}))
Expand All @@ -602,6 +653,7 @@ func TestQueryStringPrint(t *testing.T) {
"FirstField matches the function",
"SecondField is nil",
"ThirdField matches the regular expression test",
"Using Index [IndexName]",
}

// map order isn't guaranteed, check if all needed lines exist
Expand Down Expand Up @@ -803,24 +855,6 @@ func TestKeyMatchFunc(t *testing.T) {
})
}

func TestRecordOnIndexMatchFunc(t *testing.T) {
testWrap(t, func(store *bolthold.Store, t *testing.T) {
insertTestData(t, store)

defer func() {
if r := recover(); r == nil {
t.Fatalf("Running matchFunc against an Index did not panic when trying to access the Record!")
}
}()

var result []ItemTest
_ = store.Find(&result, bolthold.Where("Category").MatchFunc(func(ra *bolthold.RecordAccess) (bool, error) {
ra.Record()
return false, nil
}))
})
}

func TestKeyStructTag(t *testing.T) {
testWrap(t, func(store *bolthold.Store, t *testing.T) {
type KeyTest struct {
Expand Down
50 changes: 21 additions & 29 deletions index.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package bolthold

import (
"bytes"
"reflect"
"sort"

"github.com/boltdb/bolt"
Expand Down Expand Up @@ -171,10 +172,11 @@ func newIterator(tx *bolt.Tx, typeName string, query *Query) *iterator {
return iter
}

criteria := query.fieldCriteria[query.index]

// Key field
if query.index == Key {
if query.index == Key && !query.badIndex {
iter.indexCursor = tx.Bucket([]byte(typeName)).Cursor()
criteria := query.fieldCriteria[Key]

iter.nextKeys = func(prepCursor bool, cursor *bolt.Cursor) ([][]byte, error) {
var nKeys [][]byte
Expand All @@ -191,7 +193,14 @@ func newIterator(tx *bolt.Tx, typeName string, query *Query) *iterator {
return nKeys, nil
}

ok, err := matchesAllCriteria(criteria, k, true)
val := reflect.New(query.dataType)
v := iter.dataBucket.Get(k)
err := decode(v, val.Interface())
if err != nil {
return nil, err
}

ok, err := matchesAllCriteria(criteria, k, true, val.Interface())
if err != nil {
return nil, err
}
Expand All @@ -206,10 +215,13 @@ func newIterator(tx *bolt.Tx, typeName string, query *Query) *iterator {
return iter
}

iBucket := findIndexBucket(tx, typeName, query)
var iBucket *bolt.Bucket
if !query.badIndex {
iBucket = tx.Bucket(indexBucketName(typeName, query.index))
}

if iBucket == nil {
// bad index, filter through entire store
if iBucket == nil || hasMatchFunc(criteria) {
// bad index or matches Function on indexed field, filter through entire store
query.badIndex = true

iter.indexCursor = tx.Bucket([]byte(typeName)).Cursor()
Expand Down Expand Up @@ -242,7 +254,6 @@ func newIterator(tx *bolt.Tx, typeName string, query *Query) *iterator {

iter.nextKeys = func(prepCursor bool, cursor *bolt.Cursor) ([][]byte, error) {
var nKeys [][]byte
criteria := query.fieldCriteria[query.index]

for len(nKeys) < iteratorKeyMinCacheSize {
var k, v []byte
Expand All @@ -255,7 +266,9 @@ func newIterator(tx *bolt.Tx, typeName string, query *Query) *iterator {
if k == nil {
return nKeys, nil
}
ok, err := matchesAllCriteria(criteria, k, true)

// no currentRow on indexes as it refers to multiple rows
ok, err := matchesAllCriteria(criteria, k, true, nil)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -300,27 +313,6 @@ func seekCursor(cursor *bolt.Cursor, criteria []*Criterion) (key, value []byte)
return cursor.First()
}

// findIndexBucket returns the index bucket from the query, and if not found, tries to find the next available index
func findIndexBucket(tx *bolt.Tx, typeName string, query *Query) *bolt.Bucket {
iBucket := tx.Bucket(indexBucketName(typeName, query.index))
if iBucket != nil {
return iBucket
}

for field := range query.fieldCriteria {
if field == query.index {
continue
}

iBucket = tx.Bucket(indexBucketName(typeName, field))
if iBucket != nil {
query.index = field
return iBucket
}
}
return nil
}

// Next returns the next key value that matches the iterators criteria
// If no more kv's are available the return nil, if there is an error, they return nil
// and iterator.Error() will return the error
Expand Down
Loading

0 comments on commit bb7fe75

Please sign in to comment.