Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,9 @@ type IndexSchema struct {
// exist from a structure.
AllowMissing bool

Unique bool
Unique bool
EnforceUniqueness bool
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the naming of this is a bit confusing, like, why wouldn't the Unique val already require this? Maybe something like DisallowUpdatesViaInsert?


Indexer Indexer
}

Expand Down
51 changes: 51 additions & 0 deletions txn.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ func (txn *Txn) Insert(table string, obj interface{}) error {
// Lookup the object by ID first, to see if this is an update
idTxn := txn.writableIndex(table, id)
existing, update := idTxn.Get(idVal)
if idSchema.EnforceUniqueness && update {
return fmt.Errorf("unique constraint violated for index: 'id'")
}

// On an update, there is an existing object with the given
// primary ID. We do the update by deleting the current object
Expand Down Expand Up @@ -213,6 +216,15 @@ func (txn *Txn) Insert(table string, obj interface{}) error {
}
}

if indexSchema.EnforceUniqueness && indexSchema.Unique {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this redundant with the check above?

for _, v := range vals {
if _, exists := indexTxn.Get(v); exists {
return fmt.Errorf("unique constraint violated"+
"for index: '%s'", indexSchema.Name)
}
}
}

// Handle the update by deleting from the index first
if update {
var (
Expand Down Expand Up @@ -268,6 +280,45 @@ func (txn *Txn) Insert(table string, obj interface{}) error {
return nil
}

// Update is used to update an object into the given table
func (txn *Txn) Update(table string, obj interface{}) error {
if !txn.write {
return fmt.Errorf("cannot update in read-only transaction")
}

// Get the table schema
tableSchema, ok := txn.db.schema.Tables[table]
if !ok {
return fmt.Errorf("invalid table '%s'", table)
}

// Get the primary ID of the object
idSchema := tableSchema.Indexes[id]
idIndexer := idSchema.Indexer.(SingleIndexer)
ok, idVal, err := idIndexer.FromObject(obj)
if err != nil {
return fmt.Errorf("failed to build primary index: %v", err)
}
if !ok {
return fmt.Errorf("object missing primary index")
}

// Lookup the object by ID first, to see if this is an update
idTxn := txn.writableIndex(table, id)
existing, exists := idTxn.Get(idVal)

if !exists {
return fmt.Errorf("object not found")
}
if err := txn.Delete(table, existing); err != nil {
return fmt.Errorf("failed to delete object for update: %v", err)
}
if err := txn.Insert(table, obj); err != nil {
return fmt.Errorf("failed to insert object for update: %v", err)
}
return nil
}

// Delete is used to delete a single object from the given table
// This object must already exist in the table
func (txn *Txn) Delete(table string, obj interface{}) error {
Expand Down
98 changes: 98 additions & 0 deletions txn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1354,3 +1354,101 @@ func TestStringFieldIndexerEmptyPointerFromArgs(t *testing.T) {
}
})
}

func TestTxn_Insert_Enforce_Unique(t *testing.T) {
schema := &DBSchema{
Tables: map[string]*TableSchema{
"main": &TableSchema{
Name: "main",
Indexes: map[string]*IndexSchema{
"id": &IndexSchema{
Name: "id",
Unique: true,
EnforceUniqueness: true,
Indexer: &StringFieldIndex{Field: "ID"},
},
"foo": &IndexSchema{
Name: "foo",
Indexer: &StringFieldIndex{Field: "Foo"},
Unique: true,
EnforceUniqueness: true,
},
"baz": &IndexSchema{
Name: "baz",
Indexer: &StringFieldIndex{Field: "Baz"},
AllowMissing: true,
},
"qux": &IndexSchema{
Name: "qux",
Indexer: &StringSliceFieldIndex{Field: "Qux"},
},
},
},
},
}
db, err := NewMemDB(schema)
if err != nil {
t.Fatalf("err: %v", err)
}

txn := db.Txn(true)

defer txn.Commit()

// First Insert
obj := &TestObject{
ID: "my-object",
Foo: "abc",
Qux: []string{"abc1", "abc2"},
}
err = txn.Insert("main", obj)
if err != nil {
t.Fatalf("err: %v", err)
}

raw, err := txn.First("main", "id", obj.ID)
if err != nil {
t.Fatalf("err: %v", err)
}

if raw != obj {
t.Fatalf("bad: %#v %#v", raw, obj)
}

// re-insert should be error
err = txn.Insert("main", obj)
if err == nil {
t.Fatalf("bad: %v", err)
}

obj2 := &TestObject{
ID: "my-other-object",
Foo: "abc", // same and hence conflicting key
Qux: []string{"xyz1", "xyz2"},
}
err = txn.Insert("main", obj2)
if err == nil {
t.Fatalf("bad: %v", err)
}

// make sure original obj is good
raw, err = txn.First("main", "id", obj.ID)

if err != nil {
t.Fatalf("err: %v", err)
}
if raw != obj {
t.Fatalf("bad: %#v %#v", raw, obj)
}

// add with missing key
obj2 = &TestObject{
ID: "my-yet-another-object",
Qux: []string{"xyz1", "xyz2"},
}
err = txn.Insert("main", obj2)
if err == nil {
t.Fatalf("bad: %v", err)
}

}