Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unique Constraint Support #7

Merged
merged 3 commits into from
Feb 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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