Skip to content

Commit

Permalink
Merge pull request timshannon#7 from timshannon/development
Browse files Browse the repository at this point in the history
Unique Constraint Support
  • Loading branch information
timshannon committed Feb 19, 2019
2 parents e905920 + d573d45 commit 46953c0
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 25 deletions.
25 changes: 20 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,18 @@ your query criteria if you create an index on the Division field. The downside
on every write operation. For read heavy operations datasets, indexes can be very useful.

In every BadgerHold store, there will be a reserved bucket *_indexes* which will be used to hold indexes that point back
to another bucket's Key system. Indexes will be defined by setting the `badgerholdIndex` struct tag on a field in a type.
to another bucket's Key system. Indexes will be defined by setting the `badgerhold:"index"` struct tag on a field in a type.

```Go
type Person struct {
Name string
Division string `badgerholdIndex:"Division"`
Division string `badgerhold:"index"`
}

// alternate struct tag if you wish to specify the index name
type Person struct {
Name string
Division string `badgerholdIndex:"IdxDivision"`
}

```
Expand Down Expand Up @@ -138,16 +144,25 @@ store.UpdateMatching(&Person{}, badgerhold.Where("Death").Lt(badgerhold.Field("B
### 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 `badgerholdKey` struct tag when running `Find` queries.
automatically populate a record's Key in a struct by using the `badgerhold:"key"` struct tag when running `Find` queries.

Another common scenario is to insert data with an auto-incrementing key assigned by the database.
When performing an `Insert`, if the type of the key matches the type of the `badgerholdKey` tagged field,
When performing an `Insert`, if the type of the key matches the type of the `badgerhold:"key"` tagged field,
the data is passed in by reference, **and** the field's current value is the zero-value for that type,
then it is set on the data _before_ insertion.

```Go
type Employee struct {
ID string `badgerholdKey:"ID"` // the tagName isn't required, but some linters will complain without it
ID uint64 `badgerhold:"key"`
FirstName string
LastName string
Division string
Hired time.Time
}

// old struct tag, currenty still supported but may be deprecated in the future
type Employee struct {
ID uint64 `badgerholdKey`
FirstName string
LastName string
Division string
Expand Down
14 changes: 9 additions & 5 deletions index.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@ import (
"github.com/dgraph-io/badger"
)

// BadgerHoldIndexTag is the struct tag used to define an a field as indexable for a badgerhold
const BadgerHoldIndexTag = "badgerholdIndex"

const indexPrefix = "_bhIndex"

// size of iterator keys stored in memory before more are fetched
const iteratorKeyMinCacheSize = 100

// Index is a function that returns the indexable, encoded bytes of the passed in value
type Index func(name string, value interface{}) ([]byte, error)
type Index struct {
IndexFunc func(name string, value interface{}) ([]byte, error)
Unique bool
}

// adds an item to the index
func indexAdd(storer Storer, tx *badger.Txn, key []byte, data interface{}) error {
Expand Down Expand Up @@ -54,7 +54,8 @@ func indexDelete(storer Storer, tx *badger.Txn, key []byte, originalData interfa
// // adds or removes a specific index on an item
func indexUpdate(typeName, indexName string, index Index, tx *badger.Txn, key []byte, value interface{},
delete bool) error {
indexKey, err := index(indexName, value)

indexKey, err := index.IndexFunc(indexName, value)
if indexKey == nil {
return nil
}
Expand All @@ -73,6 +74,9 @@ func indexUpdate(typeName, indexName string, index Index, tx *badger.Txn, key []
}

if err != badger.ErrKeyNotFound {
if index.Unique && !delete {
return ErrUniqueExists
}
err = item.Value(func(iVal []byte) error {
return decode(iVal, &indexValue)
})
Expand Down
8 changes: 5 additions & 3 deletions put.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import (
// ErrKeyExists is the error returned when data is being Inserted for a Key that already exists
var ErrKeyExists = errors.New("This Key already exists in badgerhold for this type")

// ErrUniqueExists is the error thrown when data is being inserted for a unique constraint value that already exists
var ErrUniqueExists = errors.New("This value cannot be written due to the unique constraint on the field")

// sequence tells badgerhold to insert the key as the next sequence in the bucket
type sequence struct{}

Expand Down Expand Up @@ -88,9 +91,8 @@ func (s *Store) TxInsert(tx *badger.Txn, key, data interface{}) error {

for i := 0; i < dataType.NumField(); i++ {
tf := dataType.Field(i)
// XXX: should we require standard tag format so we can use StructTag.Lookup()?
// XXX: should we use strings.Contains(string(tf.Tag), badgerholdKeyTag) so we don't require proper tags?
if _, ok := tf.Tag.Lookup(BadgerholdKeyTag); ok {
if _, ok := tf.Tag.Lookup(BadgerholdKeyTag); ok ||
tf.Tag.Get(badgerholdPrefixTag) == badgerholdPrefixKeyValue {
fieldValue := dataVal.Field(i)
keyValue := reflect.ValueOf(key)
if keyValue.Type() != tf.Type {
Expand Down
138 changes: 138 additions & 0 deletions put_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -519,3 +519,141 @@ func TestInsertSetKey(t *testing.T) {

})
}

func TestAlternateTags(t *testing.T) {
testWrap(t, func(store *badgerhold.Store, t *testing.T) {
type TestAlternate struct {
Key uint64 `badgerhold:"key"`
Name string `badgerhold:"index"`
}
item := TestAlternate{
Name: "TestName",
}

key := uint64(123)
err := store.Insert(key, &item)
if err != nil {
t.Fatalf("Error inserting data for alternate tag test: %s", err)
}

if item.Key != key {
t.Fatalf("Key was not set. Wanted %d, got %d", key, item.Key)
}

var result []TestAlternate

err = store.Find(&result, badgerhold.Where("Name").Eq(item.Name).Index("Name"))
if err != nil {
t.Fatalf("Query on alternate tag index failed: %s", err)
}

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

func TestUniqueConstraint(t *testing.T) {
testWrap(t, func(store *badgerhold.Store, t *testing.T) {
type TestUnique struct {
Key uint64 `badgerhold:"key"`
Name string `badgerhold:"unique"`
}

item := &TestUnique{
Name: "Tester Name",
}

err := store.Insert(badgerhold.NextSequence(), item)
if err != nil {
t.Fatalf("Error inserting base record for unique testing: %s", err)
}

t.Run("Insert", func(t *testing.T) {
err = store.Insert(badgerhold.NextSequence(), item)
if err != badgerhold.ErrUniqueExists {
t.Fatalf("Inserting duplicate record did not result in a unique constraint error: "+
"Expected %s, Got %s", badgerhold.ErrUniqueExists, err)
}
})

t.Run("Update", func(t *testing.T) {
update := &TestUnique{
Name: "Update Name",
}
err = store.Insert(badgerhold.NextSequence(), update)

if err != nil {
t.Fatalf("Inserting record for update Unique testing failed: %s", err)
}
update.Name = item.Name

err = store.Update(update.Key, update)
if err != badgerhold.ErrUniqueExists {
t.Fatalf("Duplicate record did not result in a unique constraint error: "+
"Expected %s, Got %s", badgerhold.ErrUniqueExists, err)
}
})

t.Run("Upsert", func(t *testing.T) {
update := &TestUnique{
Name: "Upsert Name",
}
err = store.Insert(badgerhold.NextSequence(), update)

if err != nil {
t.Fatalf("Inserting record for upsert Unique testing failed: %s", err)
}

update.Name = item.Name

err = store.Upsert(update.Key, update)
if err != badgerhold.ErrUniqueExists {
t.Fatalf("Duplicate record did not result in a unique constraint error: "+
"Expected %s, Got %s", badgerhold.ErrUniqueExists, err)
}
})

t.Run("UpdateMatching", func(t *testing.T) {
update := &TestUnique{
Name: "UpdateMatching Name",
}
err = store.Insert(badgerhold.NextSequence(), update)

if err != nil {
t.Fatalf("Inserting record for updatematching Unique testing failed: %s", err)
}

err = store.UpdateMatching(TestUnique{}, badgerhold.Where(badgerhold.Key).Eq(update.Key),
func(r interface{}) error {
record, ok := r.(*TestUnique)
if !ok {
return fmt.Errorf("Record isn't the correct type! Got %T",
r)
}

record.Name = item.Name

return nil
})
if err != badgerhold.ErrUniqueExists {
t.Fatalf("Duplicate record did not result in a unique constraint error: "+
"Expected %s, Got %s", badgerhold.ErrUniqueExists, err)
}

})

t.Run("Delete", func(t *testing.T) {
err = store.Delete(item.Key, TestUnique{})
if err != nil {
t.Fatalf("Error deleting record for unique testing %s", err)
}

err = store.Insert(badgerhold.NextSequence(), item)
if err != nil {
t.Fatalf("Error inserting duplicate record that has been previously removed: %s", err)
}
})

})
}
6 changes: 2 additions & 4 deletions query.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ const (
// Where(badgerhold.Key).Eq("testkey")
const Key = ""

// BadgerholdKeyTag is the struct tag used to define an a field as a key for use in a Find query
const BadgerholdKeyTag = "badgerholdKey"

// Query is a chained collection of criteria of which an object in the badgerhold needs to match to be returned
// an empty query matches against all records
type Query struct {
Expand Down Expand Up @@ -787,7 +784,8 @@ func findQuery(tx *badger.Txn, result interface{}, query *Query) error {
var keyField string

for i := 0; i < tp.NumField(); i++ {
if strings.Contains(string(tp.Field(i).Tag), BadgerholdKeyTag) {
if strings.Contains(string(tp.Field(i).Tag), BadgerholdKeyTag) ||
tp.Field(i).Tag.Get(badgerholdPrefixTag) == badgerholdPrefixKeyValue {
keyType = tp.Field(i).Type
keyField = tp.Field(i).Name
break
Expand Down
46 changes: 38 additions & 8 deletions store.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ import (
"github.com/dgraph-io/badger"
)

const (
// BadgerHoldIndexTag is the struct tag used to define an a field as indexable for a badgerhold
BadgerHoldIndexTag = "badgerholdIndex"

// BadgerholdKeyTag is the struct tag used to define an a field as a key for use in a Find query
BadgerholdKeyTag = "badgerholdKey"

// badgerholdPrefixTag is the prefix for an alternate (more standard) version of a struct tag
badgerholdPrefixTag = "badgerhold"
badgerholdPrefixIndexValue = "index"
badgerholdPrefixKeyValue = "key"
badgerholdPrefixUniqueValue = "unique"
)

// Store is a badgerhold wrapper around a badger DB
type Store struct {
db *badger.DB
Expand Down Expand Up @@ -136,20 +150,36 @@ func newStorer(dataType interface{}) Storer {
}

for i := 0; i < storer.rType.NumField(); i++ {

indexName := ""
unique := false

if strings.Contains(string(storer.rType.Field(i).Tag), BadgerHoldIndexTag) {
indexName := storer.rType.Field(i).Tag.Get(BadgerHoldIndexTag)
indexName = storer.rType.Field(i).Tag.Get(BadgerHoldIndexTag)

if indexName != "" {
indexName = storer.rType.Field(i).Name
}
} else if tag := storer.rType.Field(i).Tag.Get(badgerholdPrefixTag); tag != "" {
if tag == badgerholdPrefixIndexValue {
indexName = storer.rType.Field(i).Name
} else if tag == badgerholdPrefixUniqueValue {
indexName = storer.rType.Field(i).Name
unique = true
}
}

storer.indexes[indexName] = func(name string, value interface{}) ([]byte, error) {
tp := reflect.ValueOf(value)
for tp.Kind() == reflect.Ptr {
tp = tp.Elem()
}

return encode(tp.FieldByName(name).Interface())
if indexName != "" {
storer.indexes[indexName] = Index{
IndexFunc: func(name string, value interface{}) ([]byte, error) {
tp := reflect.ValueOf(value)
for tp.Kind() == reflect.Ptr {
tp = tp.Elem()
}

return encode(tp.FieldByName(name).Interface())
},
Unique: unique,
}
}
}
Expand Down

0 comments on commit 46953c0

Please sign in to comment.