Skip to content

Commit

Permalink
Added unique constraint support
Browse files Browse the repository at this point in the history
* Added tests
  • Loading branch information
timshannon committed Feb 20, 2019
1 parent 75ea0ed commit fddbc22
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 22 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ then it is set on the data _before_ insertion.

```Go
type Employee struct {
ID string `badgerhold:"key"`
ID uint64 `badgerhold:"key"`
FirstName string
LastName string
Division string
Expand All @@ -162,7 +162,7 @@ type Employee struct {

// old struct tag, currenty still supported but may be deprecated in the future
type Employee struct {
ID string `badgerholdKey`
ID uint64 `badgerholdKey`
FirstName string
LastName string
Division string
Expand Down
11 changes: 9 additions & 2 deletions index.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ const indexPrefix = "_bhIndex"
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 @@ -51,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 @@ -70,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
5 changes: 4 additions & 1 deletion 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 @@ -89,7 +92,7 @@ func (s *Store) TxInsert(tx *badger.Txn, key, data interface{}) error {
for i := 0; i < dataType.NumField(); i++ {
tf := dataType.Field(i)
if _, ok := tf.Tag.Lookup(BadgerholdKeyTag); ok ||
tf.Tag.Get(BadgerholdPrefixTag) == badgerholdPrefixKeyValue {
tf.Tag.Get(badgerholdPrefixTag) == badgerholdPrefixKeyValue {
fieldValue := dataVal.Field(i)
keyValue := reflect.ValueOf(key)
if keyValue.Type() != tf.Type {
Expand Down
109 changes: 107 additions & 2 deletions put_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -523,14 +523,14 @@ func TestInsertSetKey(t *testing.T) {
func TestAlternateTags(t *testing.T) {
testWrap(t, func(store *badgerhold.Store, t *testing.T) {
type TestAlternate struct {
Key uint `badgerhold:"key"`
Key uint64 `badgerhold:"key"`
Name string `badgerhold:"index"`
}
item := TestAlternate{
Name: "TestName",
}

key := uint(123)
key := uint64(123)
err := store.Insert(key, &item)
if err != nil {
t.Fatalf("Error inserting data for alternate tag test: %s", err)
Expand All @@ -552,3 +552,108 @@ func TestAlternateTags(t *testing.T) {
}
})
}

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)
}
})

})
}
2 changes: 1 addition & 1 deletion query.go
Original file line number Diff line number Diff line change
Expand Up @@ -785,7 +785,7 @@ func findQuery(tx *badger.Txn, result interface{}, query *Query) error {

for i := 0; i < tp.NumField(); i++ {
if strings.Contains(string(tp.Field(i).Tag), BadgerholdKeyTag) ||
tp.Field(i).Tag.Get(BadgerholdPrefixTag) == badgerholdPrefixKeyValue {
tp.Field(i).Tag.Get(badgerholdPrefixTag) == badgerholdPrefixKeyValue {
keyType = tp.Field(i).Type
keyField = tp.Field(i).Name
break
Expand Down
34 changes: 20 additions & 14 deletions store.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,8 @@ const (
// BadgerholdKeyTag is the struct tag used to define an a field as a key for use in a Find query
BadgerholdKeyTag = "badgerholdKey"

// BadgerholdUniqueTag is the struct tag used to define a unique constraint on a specific field
BadgerholdUniqueTag = "badgerholdUnique"

// BadgerholdPrefixTag is the prefix for an alternate (more standard) version of a struct tag
BadgerholdPrefixTag = "badgerhold"
// badgerholdPrefixTag is the prefix for an alternate (more standard) version of a struct tag
badgerholdPrefixTag = "badgerhold"
badgerholdPrefixIndexValue = "index"
badgerholdPrefixKeyValue = "key"
badgerholdPrefixUniqueValue = "unique"
Expand Down Expand Up @@ -155,25 +152,34 @@ 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)

if indexName != "" {
indexName = storer.rType.Field(i).Name
}
} else if storer.rType.Field(i).Tag.Get(BadgerholdPrefixTag) == badgerholdPrefixIndexValue {
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
}
}

if indexName != "" {
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())
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 fddbc22

Please sign in to comment.