Repository: timshannon/bolthold (go)

Issue:
Unique index
Is there a way to define unique index? Badgerhold has the "unique" flag in index definition via Indexes() method but botlhold is missing it .

Pull request:
Unique Constraints
Closes #122

Patch:
diff --git a/README.md b/README.md
index 528b36e..9b67349 100644
--- a/README.md
+++ b/README.md
@@ -256,6 +256,20 @@ t := []string{"1", "2", "3", "4"}
 where := bolthold.Where("Id").In(bolthold.Slice(t)...)
 ```
 
+### Unique Constraints
+
+You can create a unique constraint on a given field by using the `boltholdUnique:"ConstraintName"` struct tag:
+
+```Go
+type User struct {
+  Name string
+  Email string `boltholdUnique:"UniqueEmail"` // this field will be indexed with a unique constraint
+}
+```
+
+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 `bolthold.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 BoltHold you can accomplish the same thing by calling ForEach:
diff --git a/index.go b/index.go
index a09b16b..a53a000 100644
--- a/index.go
+++ b/index.go
@@ -15,6 +15,9 @@ import (
 // BoltholdIndexTag is the struct tag used to define a field as indexable for a bolthold
 const BoltholdIndexTag = "boltholdIndex"
 
+// BoltholdUniqueTag is the struct tag used to define a field as unique constraint
+const BoltholdUniqueTag = "boltholdUnique"
+
 // BoltholdSliceIndexTag is the struct tag used to define a slice field as indexable, where each item in the
 // slice is indexed separately rather than as one index
 const BoltholdSliceIndexTag = "boltholdSliceIndex"
@@ -25,7 +28,10 @@ const indexBucketPrefix = "_index"
 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
+}
 
 // SliceIndex is a function that returns all of the indexable values in a slice
 type SliceIndex func(name string, value interface{}) ([][]byte, error)
@@ -44,14 +50,14 @@ func (s *Store) deleteIndexes(storer Storer, source BucketSource, key []byte, or
 func (s *Store) updateIndexes(storer Storer, source BucketSource, key []byte, data interface{}, delete bool) error {
 	indexes := storer.Indexes()
 	for name, index := range indexes {
-		indexKey, err := index(name, data)
+		indexKey, err := index.IndexFunc(name, data)
 		if err != nil {
 			return err
 		}
 		if indexKey == nil {
 			continue
 		}
-		err = s.updateIndex(storer.Type(), name, indexKey, source, key, delete)
+		err = s.updateIndex(storer.Type(), name, index.Unique, indexKey, source, key, delete)
 		if err != nil {
 			return err
 		}
@@ -68,7 +74,7 @@ func (s *Store) updateIndexes(storer Storer, source BucketSource, key []byte, da
 			if indexKeys[i] == nil {
 				continue
 			}
-			err = s.updateIndex(storer.Type(), name, indexKeys[i], source, key, delete)
+			err = s.updateIndex(storer.Type(), name, false, indexKeys[i], source, key, delete)
 			if err != nil {
 				return err
 			}
@@ -79,9 +85,8 @@ func (s *Store) updateIndexes(storer Storer, source BucketSource, key []byte, da
 }
 
 // adds or removes a specific index on an item
-func (s *Store) updateIndex(typeName, indexName string, indexKey []byte, source BucketSource, key []byte,
+func (s *Store) updateIndex(typeName, indexName string, unique bool, indexKey []byte, source BucketSource, key []byte,
 	delete bool) error {
-
 	indexValue := make(keyList, 0)
 
 	b, err := source.CreateBucketIfNotExists(indexBucketName(typeName, indexName))
@@ -91,10 +96,15 @@ func (s *Store) updateIndex(typeName, indexName string, indexKey []byte, source
 
 	iVal := b.Get(indexKey)
 	if iVal != nil {
+		if unique && !delete {
+			return ErrUniqueExists
+		}
+
 		err = s.decode(iVal, &indexValue)
 		if err != nil {
 			return err
 		}
+
 	}
 
 	if delete {
diff --git a/put.go b/put.go
index 138c4a0..f17f5cb 100644
--- a/put.go
+++ b/put.go
@@ -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 this bolthold 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 bolthold to insert the key as the next sequence in the bucket
 type sequence struct{}
 
diff --git a/store.go b/store.go
index 164a395..5b4494d 100644
--- a/store.go
+++ b/store.go
@@ -234,12 +234,32 @@ func (t *anonStorer) addIndex(field reflect.StructField, store *Store) {
 			indexName = field.Name
 		}
 
-		t.indexes[indexName] = func(name string, value interface{}) ([]byte, error) {
-			val := findIndexValue(name, value, BoltholdIndexTag)
-			if val == nil {
-				return nil, nil
-			}
-			return store.encode(val)
+		t.indexes[indexName] = Index{
+			IndexFunc: func(name string, value interface{}) ([]byte, error) {
+				val := findIndexValue(name, value, BoltholdIndexTag)
+				if val == nil {
+					return nil, nil
+				}
+				return store.encode(val)
+			},
+			Unique: false,
+		}
+	} else if strings.Contains(string(field.Tag), BoltholdUniqueTag) {
+		indexName := field.Tag.Get(BoltholdUniqueTag)
+
+		if indexName == "" {
+			indexName = field.Name
+		}
+
+		t.indexes[indexName] = Index{
+			IndexFunc: func(name string, value interface{}) ([]byte, error) {
+				val := findIndexValue(name, value, BoltholdUniqueTag)
+				if val == nil {
+					return nil, nil
+				}
+				return store.encode(val)
+			},
+			Unique: true,
 		}
 	}
 	if strings.Contains(string(field.Tag), BoltholdSliceIndexTag) {


Test patch:
diff --git a/put_test.go b/put_test.go
index f4d2c4c..c88d3e6 100644
--- a/put_test.go
+++ b/put_test.go
@@ -545,3 +545,108 @@ func TestInsertSetKey(t *testing.T) {
 
 	})
 }
+
+func TestUniqueConstraint(t *testing.T) {
+	testWrap(t, func(store *bolthold.Store, t *testing.T) {
+		type TestUnique struct {
+			Key  uint64 `boltholdKey:"Key"`
+			Name string `boltholdUnique:"Name"`
+		}
+
+		item := &TestUnique{
+			Name: "Tester Name",
+		}
+
+		err := store.Insert(bolthold.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(bolthold.NextSequence(), item)
+			if err != bolthold.ErrUniqueExists {
+				t.Fatalf("Inserting duplicate record did not result in a unique constraint error: "+
+					"Expected %s, Got %s", bolthold.ErrUniqueExists, err)
+			}
+		})
+
+		t.Run("Update", func(t *testing.T) {
+			update := &TestUnique{
+				Name: "Update Name",
+			}
+			err = store.Insert(bolthold.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 != bolthold.ErrUniqueExists {
+				t.Fatalf("Duplicate record did not result in a unique constraint error: "+
+					"Expected %s, Got %s", bolthold.ErrUniqueExists, err)
+			}
+		})
+
+		t.Run("Upsert", func(t *testing.T) {
+			update := &TestUnique{
+				Name: "Upsert Name",
+			}
+			err = store.Insert(bolthold.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 != bolthold.ErrUniqueExists {
+				t.Fatalf("Duplicate record did not result in a unique constraint error: "+
+					"Expected %s, Got %s", bolthold.ErrUniqueExists, err)
+			}
+		})
+
+		t.Run("UpdateMatching", func(t *testing.T) {
+			update := &TestUnique{
+				Name: "UpdateMatching Name",
+			}
+			err = store.Insert(bolthold.NextSequence(), update)
+
+			if err != nil {
+				t.Fatalf("Inserting record for updatematching Unique testing failed: %s", err)
+			}
+
+			err = store.UpdateMatching(TestUnique{}, bolthold.Where(bolthold.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 != bolthold.ErrUniqueExists {
+				t.Fatalf("Duplicate record did not result in a unique constraint error: "+
+					"Expected %s, Got %s", bolthold.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(bolthold.NextSequence(), item)
+			if err != nil {
+				t.Fatalf("Error inserting duplicate record that has been previously removed: %s", err)
+			}
+		})
+
+	})
+}
diff --git a/store_test.go b/store_test.go
index 91d51e0..8240273 100644
--- a/store_test.go
+++ b/store_test.go
@@ -278,12 +278,15 @@ type Issue115 struct{ Name string }
 func (i *Issue115) Type() string { return "Item" }
 func (i *Issue115) Indexes() map[string]bolthold.Index {
 	return map[string]bolthold.Index{
-		"Name": func(_ string, value interface{}) ([]byte, error) {
-			// If the upsert wants to delete an existing value first,
-			// value could be a **Item instead of *Item
-			// panic: interface conversion: interface {} is **Item, not *Item
-			v := value.(*Issue115).Name
-			return []byte(v), nil
+		"Name": bolthold.Index{
+			IndexFunc: func(_ string, value interface{}) ([]byte, error) {
+				// If the upsert wants to delete an existing value first,
+				// value could be a **Item instead of *Item
+				// panic: interface conversion: interface {} is **Item, not *Item
+				v := value.(*Issue115).Name
+				return []byte(v), nil
+			},
+			Unique: false,
 		},
 	}
 }