diff --git a/nomad/memdb/index.go b/nomad/memdb/index.go deleted file mode 100644 index 56d07ccb1..000000000 --- a/nomad/memdb/index.go +++ /dev/null @@ -1,228 +0,0 @@ -package memdb - -import ( - "encoding/hex" - "fmt" - "reflect" - "strings" -) - -// Indexer is an interface used for defining indexes -type Indexer interface { - // FromObject is used to extract an index value from an - // object or to indicate that the index value is missing. - FromObject(raw interface{}) (bool, []byte, error) - - // ExactFromArgs is used to build an exact index lookup - // based on arguments - FromArgs(args ...interface{}) ([]byte, error) -} - -// PrefixIndexer can optionally be implemented for any -// indexes that support prefix based iteration. This may -// not apply to all indexes. -type PrefixIndexer interface { - // PrefixFromArgs returns a prefix that should be used - // for scanning based on the arguments - PrefixFromArgs(args ...interface{}) ([]byte, error) -} - -// StringFieldIndex is used to extract a field from an object -// using reflection and builds an index on that field. -type StringFieldIndex struct { - Field string - Lowercase bool -} - -func (s *StringFieldIndex) FromObject(obj interface{}) (bool, []byte, error) { - v := reflect.ValueOf(obj) - v = reflect.Indirect(v) // Derefence the pointer if any - - fv := v.FieldByName(s.Field) - if !fv.IsValid() { - return false, nil, - fmt.Errorf("field '%s' for %#v is invalid", s.Field, obj) - } - - val := fv.String() - if val == "" { - return false, nil, nil - } - - if s.Lowercase { - val = strings.ToLower(val) - } - - // Add the null character as a terminator - val += "\x00" - return true, []byte(val), nil -} - -func (s *StringFieldIndex) FromArgs(args ...interface{}) ([]byte, error) { - if len(args) != 1 { - return nil, fmt.Errorf("must provide only a single argument") - } - arg, ok := args[0].(string) - if !ok { - return nil, fmt.Errorf("argument must be a string: %#v", args[0]) - } - if s.Lowercase { - arg = strings.ToLower(arg) - } - // Add the null character as a terminator - arg += "\x00" - return []byte(arg), nil -} - -func (s *StringFieldIndex) PrefixFromArgs(args ...interface{}) ([]byte, error) { - val, err := s.FromArgs(args...) - if err != nil { - return nil, err - } - - // Strip the null terminator, the rest is a prefix - n := len(val) - if n > 0 { - return val[:n-1], nil - } - return val, nil -} - -// UUIDFieldIndex is used to extract a field from an object -// using reflection and builds an index on that field by treating -// it as a UUID. This is an optimization to using a StringFieldIndex -// as the UUID can be more compactly represented in byte form. -type UUIDFieldIndex struct { - Field string -} - -func (u *UUIDFieldIndex) FromObject(obj interface{}) (bool, []byte, error) { - v := reflect.ValueOf(obj) - v = reflect.Indirect(v) // Derefence the pointer if any - - fv := v.FieldByName(u.Field) - if !fv.IsValid() { - return false, nil, - fmt.Errorf("field '%s' for %#v is invalid", u.Field, obj) - } - - val := fv.String() - if val == "" { - return false, nil, nil - } - - buf, err := u.parseString(val) - return true, buf, err -} - -func (u *UUIDFieldIndex) FromArgs(args ...interface{}) ([]byte, error) { - if len(args) != 1 { - return nil, fmt.Errorf("must provide only a single argument") - } - switch arg := args[0].(type) { - case string: - return u.parseString(arg) - case []byte: - if len(arg) != 16 { - return nil, fmt.Errorf("byte slice must be 16 characters") - } - return arg, nil - default: - return nil, - fmt.Errorf("argument must be a string or byte slice: %#v", args[0]) - } -} - -func (u *UUIDFieldIndex) parseString(s string) ([]byte, error) { - // Verify the length - if len(s) != 36 { - return nil, fmt.Errorf("UUID must be 36 characters") - } - - // Decode each of the parts - part1, err := hex.DecodeString(s[0:8]) - if err != nil { - return nil, fmt.Errorf("Invalid UUID: %v", err) - } - - part2, err := hex.DecodeString(s[9:13]) - if err != nil { - return nil, fmt.Errorf("Invalid UUID: %v", err) - } - - part3, err := hex.DecodeString(s[14:18]) - if err != nil { - return nil, fmt.Errorf("Invalid UUID: %v", err) - } - - part4, err := hex.DecodeString(s[19:23]) - if err != nil { - return nil, fmt.Errorf("Invalid UUID: %v", err) - } - - part5, err := hex.DecodeString(s[24:]) - if err != nil { - return nil, fmt.Errorf("Invalid UUID: %v", err) - } - - // Copy into a single buffer - buf := make([]byte, 16) - copy(buf[0:4], part1) - copy(buf[4:6], part2) - copy(buf[6:8], part3) - copy(buf[8:10], part4) - copy(buf[10:16], part5) - return buf, nil -} - -// CompoundIndex is used to build an index using multiple sub-indexes -// Prefix based iteration is supported as long as the appropriate prefix -// of indexers support it. All sub-indexers are only assumed to expect -// a single argument. -type CompoundIndex struct { - Indexes []Indexer - - // AllowMissing results in an index based on only the indexers - // that return data. If true, you may end up with 2/3 columns - // indexed which might be useful for an index scan. Otherwise, - // the CompoundIndex requires all indexers to be satisfied. - AllowMissing bool -} - -func (c *CompoundIndex) FromObject(raw interface{}) (bool, []byte, error) { - var out []byte - for i, idx := range c.Indexes { - ok, val, err := idx.FromObject(raw) - if err != nil { - return false, nil, fmt.Errorf("sub-index %d error: %v", i, err) - } - if !ok { - if c.AllowMissing { - break - } else { - return false, nil, nil - } - } - out = append(out, val...) - } - return true, out, nil -} - -func (c *CompoundIndex) FromArgs(args ...interface{}) ([]byte, error) { - if len(args) != len(c.Indexes) { - return nil, fmt.Errorf("less arguments than index fields") - } - return c.PrefixFromArgs(args...) -} - -func (c *CompoundIndex) PrefixFromArgs(args ...interface{}) ([]byte, error) { - var out []byte - for i, arg := range args { - val, err := c.Indexes[i].FromArgs(arg) - if err != nil { - return nil, fmt.Errorf("sub-index %d error: %v", i, err) - } - out = append(out, val...) - } - return out, nil -} diff --git a/nomad/memdb/index_test.go b/nomad/memdb/index_test.go deleted file mode 100644 index 8d374bc6e..000000000 --- a/nomad/memdb/index_test.go +++ /dev/null @@ -1,336 +0,0 @@ -package memdb - -import ( - "bytes" - crand "crypto/rand" - "fmt" - "testing" -) - -type TestObject struct { - ID string - Foo string - Bar int - Baz string - Empty string -} - -func testObj() *TestObject { - obj := &TestObject{ - ID: "my-cool-obj", - Foo: "Testing", - Bar: 42, - Baz: "yep", - } - return obj -} - -func TestStringFieldIndex_FromObject(t *testing.T) { - obj := testObj() - indexer := StringFieldIndex{"Foo", false} - - ok, val, err := indexer.FromObject(obj) - if err != nil { - t.Fatalf("err: %v", err) - } - if string(val) != "Testing\x00" { - t.Fatalf("bad: %s", val) - } - if !ok { - t.Fatalf("should be ok") - } - - lower := StringFieldIndex{"Foo", true} - ok, val, err = lower.FromObject(obj) - if err != nil { - t.Fatalf("err: %v", err) - } - if string(val) != "testing\x00" { - t.Fatalf("bad: %s", val) - } - if !ok { - t.Fatalf("should be ok") - } - - badField := StringFieldIndex{"NA", true} - ok, val, err = badField.FromObject(obj) - if err == nil { - t.Fatalf("should get error") - } - - emptyField := StringFieldIndex{"Empty", true} - ok, val, err = emptyField.FromObject(obj) - if err != nil { - t.Fatalf("err: %v", err) - } - if ok { - t.Fatalf("should not ok") - } -} - -func TestStringFieldIndex_FromArgs(t *testing.T) { - indexer := StringFieldIndex{"Foo", false} - _, err := indexer.FromArgs() - if err == nil { - t.Fatalf("should get err") - } - - _, err = indexer.FromArgs(42) - if err == nil { - t.Fatalf("should get err") - } - - val, err := indexer.FromArgs("foo") - if err != nil { - t.Fatalf("err: %v", err) - } - if string(val) != "foo\x00" { - t.Fatalf("foo") - } - - lower := StringFieldIndex{"Foo", true} - val, err = lower.FromArgs("Foo") - if err != nil { - t.Fatalf("err: %v", err) - } - if string(val) != "foo\x00" { - t.Fatalf("foo") - } -} - -func TestStringFieldIndex_PrefixFromArgs(t *testing.T) { - indexer := StringFieldIndex{"Foo", false} - _, err := indexer.FromArgs() - if err == nil { - t.Fatalf("should get err") - } - - _, err = indexer.PrefixFromArgs(42) - if err == nil { - t.Fatalf("should get err") - } - - val, err := indexer.PrefixFromArgs("foo") - if err != nil { - t.Fatalf("err: %v", err) - } - if string(val) != "foo" { - t.Fatalf("foo") - } - - lower := StringFieldIndex{"Foo", true} - val, err = lower.PrefixFromArgs("Foo") - if err != nil { - t.Fatalf("err: %v", err) - } - if string(val) != "foo" { - t.Fatalf("foo") - } -} - -func TestUUIDFeldIndex_parseString(t *testing.T) { - u := &UUIDFieldIndex{} - _, err := u.parseString("invalid") - if err == nil { - t.Fatalf("should error") - } - - buf, uuid := generateUUID() - - out, err := u.parseString(uuid) - if err != nil { - t.Fatalf("err: %v", err) - } - - if !bytes.Equal(out, buf) { - t.Fatalf("bad: %#v %#v", out, buf) - } -} - -func TestUUIDFieldIndex_FromObject(t *testing.T) { - obj := testObj() - uuidBuf, uuid := generateUUID() - obj.Foo = uuid - indexer := &UUIDFieldIndex{"Foo"} - - ok, val, err := indexer.FromObject(obj) - if err != nil { - t.Fatalf("err: %v", err) - } - if !bytes.Equal(uuidBuf, val) { - t.Fatalf("bad: %s", val) - } - if !ok { - t.Fatalf("should be ok") - } - - badField := &UUIDFieldIndex{"NA"} - ok, val, err = badField.FromObject(obj) - if err == nil { - t.Fatalf("should get error") - } - - emptyField := &UUIDFieldIndex{"Empty"} - ok, val, err = emptyField.FromObject(obj) - if err != nil { - t.Fatalf("err: %v", err) - } - if ok { - t.Fatalf("should not ok") - } -} - -func TestUUIDFieldIndex_FromArgs(t *testing.T) { - indexer := &UUIDFieldIndex{"Foo"} - _, err := indexer.FromArgs() - if err == nil { - t.Fatalf("should get err") - } - - _, err = indexer.FromArgs(42) - if err == nil { - t.Fatalf("should get err") - } - - uuidBuf, uuid := generateUUID() - - val, err := indexer.FromArgs(uuid) - if err != nil { - t.Fatalf("err: %v", err) - } - if !bytes.Equal(uuidBuf, val) { - t.Fatalf("foo") - } - - val, err = indexer.FromArgs(uuidBuf) - if err != nil { - t.Fatalf("err: %v", err) - } - if !bytes.Equal(uuidBuf, val) { - t.Fatalf("foo") - } -} - -func generateUUID() ([]byte, string) { - buf := make([]byte, 16) - if _, err := crand.Read(buf); err != nil { - panic(fmt.Errorf("failed to read random bytes: %v", err)) - } - uuid := fmt.Sprintf("%08x-%04x-%04x-%04x-%12x", - buf[0:4], - buf[4:6], - buf[6:8], - buf[8:10], - buf[10:16]) - return buf, uuid -} - -func TestCompoundIndex_FromObject(t *testing.T) { - obj := testObj() - indexer := &CompoundIndex{ - Indexes: []Indexer{ - &StringFieldIndex{"ID", false}, - &StringFieldIndex{"Foo", false}, - &StringFieldIndex{"Baz", false}, - }, - AllowMissing: false, - } - - ok, val, err := indexer.FromObject(obj) - if err != nil { - t.Fatalf("err: %v", err) - } - if string(val) != "my-cool-obj\x00Testing\x00yep\x00" { - t.Fatalf("bad: %s", val) - } - if !ok { - t.Fatalf("should be ok") - } - - missing := &CompoundIndex{ - Indexes: []Indexer{ - &StringFieldIndex{"ID", false}, - &StringFieldIndex{"Foo", true}, - &StringFieldIndex{"Empty", false}, - }, - AllowMissing: true, - } - ok, val, err = missing.FromObject(obj) - if err != nil { - t.Fatalf("err: %v", err) - } - if string(val) != "my-cool-obj\x00testing\x00" { - t.Fatalf("bad: %s", val) - } - if !ok { - t.Fatalf("should be ok") - } - - // Test when missing not allowed - missing.AllowMissing = false - ok, _, err = missing.FromObject(obj) - if err != nil { - t.Fatalf("err: %v", err) - } - if ok { - t.Fatalf("should not be okay") - } -} - -func TestCompoundIndex_FromArgs(t *testing.T) { - indexer := &CompoundIndex{ - Indexes: []Indexer{ - &StringFieldIndex{"ID", false}, - &StringFieldIndex{"Foo", false}, - &StringFieldIndex{"Baz", false}, - }, - AllowMissing: false, - } - _, err := indexer.FromArgs() - if err == nil { - t.Fatalf("should get err") - } - - _, err = indexer.FromArgs(42, 42, 42) - if err == nil { - t.Fatalf("should get err") - } - - val, err := indexer.FromArgs("foo", "bar", "baz") - if err != nil { - t.Fatalf("err: %v", err) - } - if string(val) != "foo\x00bar\x00baz\x00" { - t.Fatalf("bad: %s", val) - } -} - -func TestCompoundIndex_PrefixFromArgs(t *testing.T) { - indexer := &CompoundIndex{ - Indexes: []Indexer{ - &UUIDFieldIndex{"ID"}, - &StringFieldIndex{"Foo", false}, - &StringFieldIndex{"Baz", false}, - }, - AllowMissing: false, - } - val, err := indexer.PrefixFromArgs() - if err != nil { - t.Fatalf("err: %v", err) - } - if len(val) != 0 { - t.Fatalf("bad: %s", val) - } - - uuidBuf, uuid := generateUUID() - val, err = indexer.PrefixFromArgs(uuid, "foo") - if err != nil { - t.Fatalf("err: %v", err) - } - if !bytes.Equal(val[:16], uuidBuf) { - t.Fatalf("bad prefix") - } - if string(val[16:]) != "foo\x00" { - t.Fatalf("bad: %s", val) - } -} diff --git a/nomad/memdb/integ_test.go b/nomad/memdb/integ_test.go deleted file mode 100644 index 686e403ff..000000000 --- a/nomad/memdb/integ_test.go +++ /dev/null @@ -1,314 +0,0 @@ -package memdb - -import "testing" - -// Test that multiple concurrent transactions are isolated from each other -func TestTxn_Isolation(t *testing.T) { - db := testDB(t) - txn1 := db.Txn(true) - - obj := &TestObject{ - ID: "my-object", - Foo: "abc", - } - obj2 := &TestObject{ - ID: "my-cool-thing", - Foo: "xyz", - } - obj3 := &TestObject{ - ID: "my-other-cool-thing", - Foo: "xyz", - } - - err := txn1.Insert("main", obj) - if err != nil { - t.Fatalf("err: %v", err) - } - err = txn1.Insert("main", obj2) - if err != nil { - t.Fatalf("err: %v", err) - } - err = txn1.Insert("main", obj3) - if err != nil { - t.Fatalf("err: %v", err) - } - - // Results should show up in this transaction - raw, err := txn1.First("main", "id") - if err != nil { - t.Fatalf("err: %v", err) - } - if raw == nil { - t.Fatalf("bad: %#v", raw) - } - - // Create a new transaction, current one is NOT committed - txn2 := db.Txn(false) - - // Nothing should show up in this transaction - raw, err = txn2.First("main", "id") - if err != nil { - t.Fatalf("err: %v", err) - } - if raw != nil { - t.Fatalf("bad: %#v", raw) - } - - // Commit txn1, txn2 should still be isolated - txn1.Commit() - - // Nothing should show up in this transaction - raw, err = txn2.First("main", "id") - if err != nil { - t.Fatalf("err: %v", err) - } - if raw != nil { - t.Fatalf("bad: %#v", raw) - } - - // Create a new txn - txn3 := db.Txn(false) - - // Results should show up in this transaction - raw, err = txn3.First("main", "id") - if err != nil { - t.Fatalf("err: %v", err) - } - if raw == nil { - t.Fatalf("bad: %#v", raw) - } -} - -// Test that an abort clears progress -func TestTxn_Abort(t *testing.T) { - db := testDB(t) - txn1 := db.Txn(true) - - obj := &TestObject{ - ID: "my-object", - Foo: "abc", - } - obj2 := &TestObject{ - ID: "my-cool-thing", - Foo: "xyz", - } - obj3 := &TestObject{ - ID: "my-other-cool-thing", - Foo: "xyz", - } - - err := txn1.Insert("main", obj) - if err != nil { - t.Fatalf("err: %v", err) - } - err = txn1.Insert("main", obj2) - if err != nil { - t.Fatalf("err: %v", err) - } - err = txn1.Insert("main", obj3) - if err != nil { - t.Fatalf("err: %v", err) - } - - // Abort the txn - txn1.Abort() - txn1.Commit() - - // Create a new transaction - txn2 := db.Txn(false) - - // Nothing should show up in this transaction - raw, err := txn2.First("main", "id") - if err != nil { - t.Fatalf("err: %v", err) - } - if raw != nil { - t.Fatalf("bad: %#v", raw) - } -} - -func TestComplexDB(t *testing.T) { - db := testComplexDB(t) - testPopulateData(t, db) - txn := db.Txn(false) // read only - - // Get using a full name - raw, err := txn.First("people", "name", "Armon", "Dadgar") - noErr(t, err) - if raw == nil { - t.Fatalf("should get person") - } - - // Get using a prefix - raw, err = txn.First("people", "name_prefix", "Armon") - noErr(t, err) - if raw == nil { - t.Fatalf("should get person") - } - - // Where in the world is mitchell hashimoto? - raw, err = txn.First("people", "name_prefix", "Mitchell") - noErr(t, err) - if raw == nil { - t.Fatalf("should get person") - } - - person := raw.(*TestPerson) - if person.First != "Mitchell" { - t.Fatalf("wrong person!") - } - - raw, err = txn.First("visits", "id_prefix", person.ID) - noErr(t, err) - if raw == nil { - t.Fatalf("should get visit") - } - - visit := raw.(*TestVisit) - - raw, err = txn.First("places", "id", visit.Place) - noErr(t, err) - if raw == nil { - t.Fatalf("should get place") - } - - place := raw.(*TestPlace) - if place.Name != "Maui" { - t.Fatalf("bad place (but isn't anywhere else really?): %v", place) - } -} - -func testPopulateData(t *testing.T, db *MemDB) { - // Start write txn - txn := db.Txn(true) - - // Create some data - person1 := testPerson() - person2 := testPerson() - person2.First = "Mitchell" - person2.Last = "Hashimoto" - - place1 := testPlace() - place2 := testPlace() - place2.Name = "Maui" - - visit1 := &TestVisit{person1.ID, place1.ID} - visit2 := &TestVisit{person2.ID, place2.ID} - - // Insert it all - noErr(t, txn.Insert("people", person1)) - noErr(t, txn.Insert("people", person2)) - noErr(t, txn.Insert("places", place1)) - noErr(t, txn.Insert("places", place2)) - noErr(t, txn.Insert("visits", visit1)) - noErr(t, txn.Insert("visits", visit2)) - - // Commit - txn.Commit() -} - -func noErr(t *testing.T, err error) { - if err != nil { - t.Fatalf("err: %v", err) - } -} - -type TestPerson struct { - ID string - First string - Last string -} - -type TestPlace struct { - ID string - Name string -} - -type TestVisit struct { - Person string - Place string -} - -func testComplexSchema() *DBSchema { - return &DBSchema{ - Tables: map[string]*TableSchema{ - "people": &TableSchema{ - Name: "people", - Indexes: map[string]*IndexSchema{ - "id": &IndexSchema{ - Name: "id", - Unique: true, - Indexer: &UUIDFieldIndex{Field: "ID"}, - }, - "name": &IndexSchema{ - Name: "name", - Unique: true, - Indexer: &CompoundIndex{ - Indexes: []Indexer{ - &StringFieldIndex{Field: "First"}, - &StringFieldIndex{Field: "Last"}, - }, - }, - }, - }, - }, - "places": &TableSchema{ - Name: "places", - Indexes: map[string]*IndexSchema{ - "id": &IndexSchema{ - Name: "id", - Unique: true, - Indexer: &UUIDFieldIndex{Field: "ID"}, - }, - "name": &IndexSchema{ - Name: "name", - Unique: true, - Indexer: &StringFieldIndex{Field: "Name"}, - }, - }, - }, - "visits": &TableSchema{ - Name: "visits", - Indexes: map[string]*IndexSchema{ - "id": &IndexSchema{ - Name: "id", - Unique: true, - Indexer: &CompoundIndex{ - Indexes: []Indexer{ - &UUIDFieldIndex{Field: "Person"}, - &UUIDFieldIndex{Field: "Place"}, - }, - }, - }, - }, - }, - }, - } -} - -func testComplexDB(t *testing.T) *MemDB { - db, err := NewMemDB(testComplexSchema()) - if err != nil { - t.Fatalf("err: %v", err) - } - return db -} - -func testPerson() *TestPerson { - _, uuid := generateUUID() - obj := &TestPerson{ - ID: uuid, - First: "Armon", - Last: "Dadgar", - } - return obj -} - -func testPlace() *TestPlace { - _, uuid := generateUUID() - obj := &TestPlace{ - ID: uuid, - Name: "HashiCorp", - } - return obj -} diff --git a/nomad/memdb/memdb.go b/nomad/memdb/memdb.go deleted file mode 100644 index 4cd47d30f..000000000 --- a/nomad/memdb/memdb.go +++ /dev/null @@ -1,68 +0,0 @@ -package memdb - -import ( - "sync" - - "github.com/hashicorp/go-immutable-radix" -) - -// MemDB is an in-memory database. It provides a table abstraction, -// which is used to store objects (rows) with multiple indexes based -// on values. The database makes use of immutable radix trees to provide -// transactions and MVCC. -type MemDB struct { - schema *DBSchema - root *iradix.Tree - - // There can only be a single writter at once - writer sync.Mutex -} - -// NewMemDB creates a new MemDB with the given schema -func NewMemDB(schema *DBSchema) (*MemDB, error) { - // Validate the schema - if err := schema.Validate(); err != nil { - return nil, err - } - - // Create the MemDB - db := &MemDB{ - schema: schema, - root: iradix.New(), - } - if err := db.initialize(); err != nil { - return nil, err - } - return db, nil -} - -// Txn is used to start a new transaction, in either read or write mode. -// There can only be a single concurrent writer, but any number of readers. -func (db *MemDB) Txn(write bool) *Txn { - if write { - db.writer.Lock() - } - txn := &Txn{ - db: db, - write: write, - rootTxn: db.root.Txn(), - } - return txn -} - -// initialize is used to setup the DB for use after creation -func (db *MemDB) initialize() error { - for tName, tableSchema := range db.schema.Tables { - for iName, _ := range tableSchema.Indexes { - index := iradix.New() - path := indexPath(tName, iName) - db.root, _, _ = db.root.Insert(path, index) - } - } - return nil -} - -// indexPath returns the path from the root to the given table index -func indexPath(table, index string) []byte { - return []byte(table + "." + index) -} diff --git a/nomad/memdb/memdb_test.go b/nomad/memdb/memdb_test.go deleted file mode 100644 index 134713d81..000000000 --- a/nomad/memdb/memdb_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package memdb - -import ( - "testing" - "time" -) - -func TestMemDB_SingleWriter_MultiReader(t *testing.T) { - db, err := NewMemDB(testValidSchema()) - if err != nil { - t.Fatalf("err: %v", err) - } - - tx1 := db.Txn(true) - tx2 := db.Txn(false) // Should not block! - tx3 := db.Txn(false) // Should not block! - tx4 := db.Txn(false) // Should not block! - - doneCh := make(chan struct{}) - go func() { - defer close(doneCh) - db.Txn(true) - }() - - select { - case <-doneCh: - t.Fatalf("should not allow another writer") - case <-time.After(10 * time.Millisecond): - } - - tx1.Abort() - tx2.Abort() - tx3.Abort() - tx4.Abort() - - select { - case <-doneCh: - case <-time.After(10 * time.Millisecond): - t.Fatalf("should allow another writer") - } -} diff --git a/nomad/memdb/schema.go b/nomad/memdb/schema.go deleted file mode 100644 index 2b8ffb476..000000000 --- a/nomad/memdb/schema.go +++ /dev/null @@ -1,76 +0,0 @@ -package memdb - -import "fmt" - -// DBSchema contains the full database schema used for MemDB -type DBSchema struct { - Tables map[string]*TableSchema -} - -// Validate is used to validate the database schema -func (s *DBSchema) Validate() error { - if s == nil { - return fmt.Errorf("missing schema") - } - if len(s.Tables) == 0 { - return fmt.Errorf("no tables defined") - } - for name, table := range s.Tables { - if name != table.Name { - return fmt.Errorf("table name mis-match for '%s'", name) - } - if err := table.Validate(); err != nil { - return err - } - } - return nil -} - -// TableSchema contains the schema for a single table -type TableSchema struct { - Name string - Indexes map[string]*IndexSchema -} - -// Validate is used to validate the table schema -func (s *TableSchema) Validate() error { - if s.Name == "" { - return fmt.Errorf("missing table name") - } - if len(s.Indexes) == 0 { - return fmt.Errorf("missing table schemas for '%s'", s.Name) - } - if _, ok := s.Indexes["id"]; !ok { - return fmt.Errorf("must have id index") - } - if !s.Indexes["id"].Unique { - return fmt.Errorf("id index must be unique") - } - for name, index := range s.Indexes { - if name != index.Name { - return fmt.Errorf("index name mis-match for '%s'", name) - } - if err := index.Validate(); err != nil { - return err - } - } - return nil -} - -// IndexSchema contains the schema for an index -type IndexSchema struct { - Name string - AllowMissing bool - Unique bool - Indexer Indexer -} - -func (s *IndexSchema) Validate() error { - if s.Name == "" { - return fmt.Errorf("missing index name") - } - if s.Indexer == nil { - return fmt.Errorf("missing index function for '%s'", s.Name) - } - return nil -} diff --git a/nomad/memdb/schema_test.go b/nomad/memdb/schema_test.go deleted file mode 100644 index 07c4dfc36..000000000 --- a/nomad/memdb/schema_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package memdb - -import "testing" - -func testValidSchema() *DBSchema { - return &DBSchema{ - Tables: map[string]*TableSchema{ - "main": &TableSchema{ - Name: "main", - Indexes: map[string]*IndexSchema{ - "id": &IndexSchema{ - Name: "id", - Unique: true, - Indexer: &StringFieldIndex{Field: "ID"}, - }, - "foo": &IndexSchema{ - Name: "foo", - Indexer: &StringFieldIndex{Field: "Foo"}, - }, - }, - }, - }, - } -} - -func TestDBSchema_Validate(t *testing.T) { - s := &DBSchema{} - err := s.Validate() - if err == nil { - t.Fatalf("should not validate, empty") - } - - s.Tables = map[string]*TableSchema{ - "foo": &TableSchema{Name: "foo"}, - } - err = s.Validate() - if err == nil { - t.Fatalf("should not validate, no indexes") - } - - valid := testValidSchema() - err = valid.Validate() - if err != nil { - t.Fatalf("should validate: %v", err) - } -} - -func TestTableSchema_Validate(t *testing.T) { - s := &TableSchema{} - err := s.Validate() - if err == nil { - t.Fatalf("should not validate, empty") - } - - s.Indexes = map[string]*IndexSchema{ - "foo": &IndexSchema{Name: "foo"}, - } - err = s.Validate() - if err == nil { - t.Fatalf("should not validate, no indexes") - } - - valid := &TableSchema{ - Name: "main", - Indexes: map[string]*IndexSchema{ - "id": &IndexSchema{ - Name: "id", - Unique: true, - Indexer: &StringFieldIndex{Field: "ID", Lowercase: true}, - }, - }, - } - err = valid.Validate() - if err != nil { - t.Fatalf("should validate: %v", err) - } -} - -func TestIndexSchema_Validate(t *testing.T) { - s := &IndexSchema{} - err := s.Validate() - if err == nil { - t.Fatalf("should not validate, empty") - } - - s.Name = "foo" - err = s.Validate() - if err == nil { - t.Fatalf("should not validate, no indexer") - } - - s.Indexer = &StringFieldIndex{Field: "Foo", Lowercase: false} - err = s.Validate() - if err != nil { - t.Fatalf("should validate: %v", err) - } -} diff --git a/nomad/memdb/txn.go b/nomad/memdb/txn.go deleted file mode 100644 index d6714aba8..000000000 --- a/nomad/memdb/txn.go +++ /dev/null @@ -1,416 +0,0 @@ -package memdb - -import ( - "fmt" - "strings" - - "github.com/hashicorp/go-immutable-radix" -) - -// tableIndex is a tuple of (Table, Index) used for lookups -type tableIndex struct { - Table string - Index string -} - -// Txn is a transaction against a MemDB. -// This can be a read or write transaction. -type Txn struct { - db *MemDB - write bool - rootTxn *iradix.Txn - - modified map[tableIndex]*iradix.Txn -} - -// readableIndex returns a transaction usable for reading the given -// index in a table. If a write transaction is in progress, we may need -// to use an existing modified txn. -func (txn *Txn) readableIndex(table, index string) *iradix.Txn { - // Look for existing transaction - if txn.write && txn.modified != nil { - key := tableIndex{table, index} - exist, ok := txn.modified[key] - if ok { - return exist - } - } - - // Create a read transaction - path := indexPath(table, index) - raw, _ := txn.rootTxn.Get(path) - indexTxn := raw.(*iradix.Tree).Txn() - return indexTxn -} - -// writableIndex returns a transaction usable for modifying the -// given index in a table. -func (txn *Txn) writableIndex(table, index string) *iradix.Txn { - if txn.modified == nil { - txn.modified = make(map[tableIndex]*iradix.Txn) - } - - // Look for existing transaction - key := tableIndex{table, index} - exist, ok := txn.modified[key] - if ok { - return exist - } - - // Start a new transaction - path := indexPath(table, index) - raw, _ := txn.rootTxn.Get(path) - indexTxn := raw.(*iradix.Tree).Txn() - - // Keep this open for the duration of the txn - txn.modified[key] = indexTxn - return indexTxn -} - -// Abort is used to cancel this transaction. -// This is a noop for read transactions. -func (txn *Txn) Abort() { - // Noop for a read transaction - if !txn.write { - return - } - - // Check if already aborted or committed - if txn.rootTxn == nil { - return - } - - // Clear the txn - txn.rootTxn = nil - txn.modified = nil - - // Release the writer lock since this is invalid - txn.db.writer.Unlock() -} - -// Commit is used to finalize this transaction. -// This is a noop for read transactions. -func (txn *Txn) Commit() { - // Noop for a read transaction - if !txn.write { - return - } - - // Check if already aborted or committed - if txn.rootTxn == nil { - return - } - - // Commit each sub-transaction scoped to (table, index) - for key, subTxn := range txn.modified { - path := indexPath(key.Table, key.Index) - final := subTxn.Commit() - txn.rootTxn.Insert(path, final) - } - - // Update the root of the DB - txn.db.root = txn.rootTxn.Commit() - - // Clear the txn - txn.rootTxn = nil - txn.modified = nil - - // Release the writer lock since this is invalid - txn.db.writer.Unlock() -} - -// Insert is used to add or update an object into the given table -func (txn *Txn) Insert(table string, obj interface{}) error { - if !txn.write { - return fmt.Errorf("cannot insert 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"] - ok, idVal, err := idSchema.Indexer.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, update := idTxn.Get(idVal) - - // On an update, there is an existing object with the given - // primary ID. We do the update by deleting the current object - // and inserting the new object. - for name, indexSchema := range tableSchema.Indexes { - indexTxn := txn.writableIndex(table, name) - - // Handle the update by deleting from the index first - if update { - ok, val, err := indexSchema.Indexer.FromObject(existing) - if err != nil { - return fmt.Errorf("failed to build index '%s': %v", name, err) - } - if ok { - // Handle non-unique index by computing a unique index. - // This is done by appending the primary key which must - // be unique anyways. - if !indexSchema.Unique { - val = append(val, idVal...) - } - indexTxn.Delete(val) - } - } - - // Handle the insert after the update - ok, val, err := indexSchema.Indexer.FromObject(obj) - if err != nil { - return fmt.Errorf("failed to build index '%s': %v", name, err) - } - if !ok { - if indexSchema.AllowMissing { - continue - } else { - return fmt.Errorf("missing value for index '%s'", name) - } - } - - // Handle non-unique index by computing a unique index. - // This is done by appending the primary key which must - // be unique anyways. - if !indexSchema.Unique { - val = append(val, idVal...) - } - indexTxn.Insert(val, obj) - } - 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 { - if !txn.write { - return fmt.Errorf("cannot delete 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"] - ok, idVal, err := idSchema.Indexer.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, check fi we should continue - idTxn := txn.writableIndex(table, "id") - existing, ok := idTxn.Get(idVal) - if !ok { - return fmt.Errorf("not found") - } - - // Remove the object from all the indexes - for name, indexSchema := range tableSchema.Indexes { - indexTxn := txn.writableIndex(table, name) - - // Handle the update by deleting from the index first - ok, val, err := indexSchema.Indexer.FromObject(existing) - if err != nil { - return fmt.Errorf("failed to build index '%s': %v", name, err) - } - if ok { - // Handle non-unique index by computing a unique index. - // This is done by appending the primary key which must - // be unique anyways. - if !indexSchema.Unique { - val = append(val, idVal...) - } - indexTxn.Delete(val) - } - } - return nil -} - -// DeleteAll is used to delete all the objects in a given table -// matching the constraints on the index -func (txn *Txn) DeleteAll(table, index string, args ...interface{}) (int, error) { - if !txn.write { - return 0, fmt.Errorf("cannot delete in read-only transaction") - } - - // TODO: Currently we use Get to just every object and then - // iterate and delete them all. This works because sliceIterator - // has the full result set, but we may need to handle the iteraction - // between the iterator and delete in the future. - - // Get all the objects - iter, err := txn.Get(table, index, args...) - if err != nil { - return 0, err - } - - // Delete all - var num int - for { - obj := iter.Next() - if obj == nil { - break - } - if err := txn.Delete(table, obj); err != nil { - return num, err - } - num++ - } - return num, nil -} - -// First is used to return the first matching object for -// the given constraints on the index -func (txn *Txn) First(table, index string, args ...interface{}) (interface{}, error) { - // Get the index value - indexSchema, val, err := txn.getIndexValue(table, index, args...) - if err != nil { - return nil, err - } - - // Get the index itself - indexTxn := txn.readableIndex(table, indexSchema.Name) - - // Do an exact lookup - if indexSchema.Unique && val != nil && indexSchema.Name == index { - obj, ok := indexTxn.Get(val) - if !ok { - return nil, nil - } - return obj, nil - } - - // Handle non-unique index by doing a prefix walk - // and getting the first value - // TODO: Optimize this - var firstVal interface{} - indexRoot := indexTxn.Root() - indexRoot.WalkPrefix(val, func(key []byte, val interface{}) bool { - firstVal = val - return true - }) - return firstVal, nil -} - -// getIndexValue is used to get the IndexSchema and the value -// used to scan the index given the parameters. This handles prefix based -// scans when the index has the "_prefix" suffix. The index must support -// prefix iteration. -func (txn *Txn) getIndexValue(table, index string, args ...interface{}) (*IndexSchema, []byte, error) { - // Get the table schema - tableSchema, ok := txn.db.schema.Tables[table] - if !ok { - return nil, nil, fmt.Errorf("invalid table '%s'", table) - } - - // Check for a prefix scan - prefixScan := false - if strings.HasSuffix(index, "_prefix") { - index = strings.TrimSuffix(index, "_prefix") - prefixScan = true - } - - // Get the index schema - indexSchema, ok := tableSchema.Indexes[index] - if !ok { - return nil, nil, fmt.Errorf("invalid index '%s'", index) - } - - // Hot-path for when there are no arguments - if len(args) == 0 { - return indexSchema, nil, nil - } - - // Special case the prefix scanning - if prefixScan { - prefixIndexer, ok := indexSchema.Indexer.(PrefixIndexer) - if !ok { - return indexSchema, nil, - fmt.Errorf("index '%s' does not support prefix scanning", index) - } - - val, err := prefixIndexer.PrefixFromArgs(args...) - if err != nil { - return indexSchema, nil, fmt.Errorf("index error: %v", err) - } - return indexSchema, val, err - } - - // Get the exact match index - val, err := indexSchema.Indexer.FromArgs(args...) - if err != nil { - return indexSchema, nil, fmt.Errorf("index error: %v", err) - } - return indexSchema, val, err -} - -// ResultIterator is used to iterate over a list of results -// from a Get query on a table. -type ResultIterator interface { - Next() interface{} -} - -// Get is used to construct a ResultIterator over all the -// rows that match the given constraints of an index. -func (txn *Txn) Get(table, index string, args ...interface{}) (ResultIterator, error) { - // Get the index value to scan - indexSchema, val, err := txn.getIndexValue(table, index, args...) - if err != nil { - return nil, err - } - - // Get the index itself - indexTxn := txn.readableIndex(table, indexSchema.Name) - indexRoot := indexTxn.Root() - - // Collect all the objects by walking the prefix. This should obviously - // be optimized by using an iterator over the radix tree, but that is - // a lot more work so its a TODO for now. - var results []interface{} - indexRoot.WalkPrefix(val, func(key []byte, val interface{}) bool { - results = append(results, val) - return false - }) - - // Create a crappy iterator - iter := &sliceIterator{ - nextIndex: 0, - results: results, - } - return iter, nil -} - -// Slice iterator is used to iterate over a slice of results. -// This is not very efficient as it means the results have already -// been materialized under the iterator. -type sliceIterator struct { - nextIndex int - results []interface{} -} - -func (s *sliceIterator) Next() interface{} { - if s.nextIndex >= len(s.results) { - return nil - } - result := s.results[s.nextIndex] - s.nextIndex++ - return result -} diff --git a/nomad/memdb/txn_test.go b/nomad/memdb/txn_test.go deleted file mode 100644 index 5cd9803b1..000000000 --- a/nomad/memdb/txn_test.go +++ /dev/null @@ -1,528 +0,0 @@ -package memdb - -import "testing" - -func testDB(t *testing.T) *MemDB { - db, err := NewMemDB(testValidSchema()) - if err != nil { - t.Fatalf("err: %v", err) - } - return db -} - -func TestTxn_Read_AbortCommit(t *testing.T) { - db := testDB(t) - txn := db.Txn(false) // Readonly - - txn.Abort() - txn.Abort() - txn.Commit() - txn.Commit() -} - -func TestTxn_Write_AbortCommit(t *testing.T) { - db := testDB(t) - txn := db.Txn(true) // Write - - txn.Abort() - txn.Abort() - txn.Commit() - txn.Commit() - - txn = db.Txn(true) // Write - - txn.Commit() - txn.Commit() - txn.Abort() - txn.Abort() -} - -func TestTxn_Insert_First(t *testing.T) { - db := testDB(t) - txn := db.Txn(true) - - obj := testObj() - 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) - } -} - -func TestTxn_InsertUpdate_First(t *testing.T) { - db := testDB(t) - txn := db.Txn(true) - - obj := &TestObject{ - ID: "my-object", - Foo: "abc", - } - 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) - } - - // Update the object - obj2 := &TestObject{ - ID: "my-object", - Foo: "xyz", - } - err = txn.Insert("main", obj2) - 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 != obj2 { - t.Fatalf("bad: %#v %#v", raw, obj) - } -} - -func TestTxn_InsertUpdate_First_NonUnique(t *testing.T) { - db := testDB(t) - txn := db.Txn(true) - - obj := &TestObject{ - ID: "my-object", - Foo: "abc", - } - err := txn.Insert("main", obj) - if err != nil { - t.Fatalf("err: %v", err) - } - - raw, err := txn.First("main", "foo", obj.Foo) - if err != nil { - t.Fatalf("err: %v", err) - } - - if raw != obj { - t.Fatalf("bad: %#v %#v", raw, obj) - } - - // Update the object - obj2 := &TestObject{ - ID: "my-object", - Foo: "xyz", - } - err = txn.Insert("main", obj2) - if err != nil { - t.Fatalf("err: %v", err) - } - - raw, err = txn.First("main", "foo", obj2.Foo) - if err != nil { - t.Fatalf("err: %v", err) - } - - if raw != obj2 { - t.Fatalf("bad: %#v %#v", raw, obj2) - } - - // Lookup of the old value should fail - raw, err = txn.First("main", "foo", obj.Foo) - if err != nil { - t.Fatalf("err: %v", err) - } - - if raw != nil { - t.Fatalf("bad: %#v", raw) - } -} - -func TestTxn_First_NonUnique_Multiple(t *testing.T) { - db := testDB(t) - txn := db.Txn(true) - - obj := &TestObject{ - ID: "my-object", - Foo: "abc", - } - obj2 := &TestObject{ - ID: "my-cool-thing", - Foo: "xyz", - } - obj3 := &TestObject{ - ID: "my-other-cool-thing", - Foo: "xyz", - } - - err := txn.Insert("main", obj) - if err != nil { - t.Fatalf("err: %v", err) - } - err = txn.Insert("main", obj2) - if err != nil { - t.Fatalf("err: %v", err) - } - err = txn.Insert("main", obj3) - if err != nil { - t.Fatalf("err: %v", err) - } - - // The first object has a unique secondary value - raw, err := txn.First("main", "foo", obj.Foo) - if err != nil { - t.Fatalf("err: %v", err) - } - if raw != obj { - t.Fatalf("bad: %#v %#v", raw, obj) - } - - // Second and third object share secondary value, - // but the primary ID of obj2 should be first - raw, err = txn.First("main", "foo", obj2.Foo) - if err != nil { - t.Fatalf("err: %v", err) - } - if raw != obj2 { - t.Fatalf("bad: %#v %#v", raw, obj2) - } -} - -func TestTxn_InsertDelete_Simple(t *testing.T) { - db := testDB(t) - txn := db.Txn(true) - - obj1 := &TestObject{ - ID: "my-cool-thing", - Foo: "xyz", - } - obj2 := &TestObject{ - ID: "my-other-cool-thing", - Foo: "xyz", - } - - err := txn.Insert("main", obj1) - if err != nil { - t.Fatalf("err: %v", err) - } - err = txn.Insert("main", obj2) - if err != nil { - t.Fatalf("err: %v", err) - } - - // Check the shared secondary value, - // but the primary ID of obj2 should be first - raw, err := txn.First("main", "foo", obj2.Foo) - if err != nil { - t.Fatalf("err: %v", err) - } - if raw != obj1 { - t.Fatalf("bad: %#v %#v", raw, obj1) - } - - // Commit and start a new transaction - txn.Commit() - txn = db.Txn(true) - - // Delete obj1 - err = txn.Delete("main", obj1) - if err != nil { - t.Fatalf("err: %v", err) - } - - // Lookup of the primary obj1 should fail - raw, err = txn.First("main", "id", obj1.ID) - if err != nil { - t.Fatalf("err: %v", err) - } - if raw != nil { - t.Fatalf("bad: %#v %#v", raw, obj1) - } - - // Commit and start a new read transaction - txn.Commit() - txn = db.Txn(false) - - // Lookup of the primary obj1 should fail - raw, err = txn.First("main", "id", obj1.ID) - if err != nil { - t.Fatalf("err: %v", err) - } - if raw != nil { - t.Fatalf("bad: %#v %#v", raw, obj1) - } - - // Check the shared secondary value, - // but the primary ID of obj2 should be first - raw, err = txn.First("main", "foo", obj2.Foo) - if err != nil { - t.Fatalf("err: %v", err) - } - if raw != obj2 { - t.Fatalf("bad: %#v %#v", raw, obj2) - } -} - -func TestTxn_InsertGet_Simple(t *testing.T) { - db := testDB(t) - txn := db.Txn(true) - - obj1 := &TestObject{ - ID: "my-cool-thing", - Foo: "xyz", - } - obj2 := &TestObject{ - ID: "my-other-cool-thing", - Foo: "xyz", - } - - err := txn.Insert("main", obj1) - if err != nil { - t.Fatalf("err: %v", err) - } - err = txn.Insert("main", obj2) - if err != nil { - t.Fatalf("err: %v", err) - } - - checkResult := func(txn *Txn) { - // Attempt a row scan on the ID - result, err := txn.Get("main", "id") - if err != nil { - t.Fatalf("err: %v", err) - } - - if raw := result.Next(); raw != obj1 { - t.Fatalf("bad: %#v %#v", raw, obj1) - } - - if raw := result.Next(); raw != obj2 { - t.Fatalf("bad: %#v %#v", raw, obj2) - } - - if raw := result.Next(); raw != nil { - t.Fatalf("bad: %#v %#v", raw, nil) - } - - // Attempt a row scan on the ID with specific ID - result, err = txn.Get("main", "id", obj1.ID) - if err != nil { - t.Fatalf("err: %v", err) - } - - if raw := result.Next(); raw != obj1 { - t.Fatalf("bad: %#v %#v", raw, obj1) - } - - if raw := result.Next(); raw != nil { - t.Fatalf("bad: %#v %#v", raw, nil) - } - - // Attempt a row scan secondary index - result, err = txn.Get("main", "foo", obj1.Foo) - if err != nil { - t.Fatalf("err: %v", err) - } - - if raw := result.Next(); raw != obj1 { - t.Fatalf("bad: %#v %#v", raw, obj1) - } - - if raw := result.Next(); raw != obj2 { - t.Fatalf("bad: %#v %#v", raw, obj2) - } - - if raw := result.Next(); raw != nil { - t.Fatalf("bad: %#v %#v", raw, nil) - } - } - - // Check the results within the txn - checkResult(txn) - - // Commit and start a new read transaction - txn.Commit() - txn = db.Txn(false) - - // Check the results in a new txn - checkResult(txn) -} - -func TestTxn_DeleteAll_Simple(t *testing.T) { - db := testDB(t) - txn := db.Txn(true) - - obj1 := &TestObject{ - ID: "my-object", - Foo: "abc", - } - obj2 := &TestObject{ - ID: "my-cool-thing", - Foo: "xyz", - } - obj3 := &TestObject{ - ID: "my-other-cool-thing", - Foo: "xyz", - } - - err := txn.Insert("main", obj1) - if err != nil { - t.Fatalf("err: %v", err) - } - err = txn.Insert("main", obj2) - if err != nil { - t.Fatalf("err: %v", err) - } - err = txn.Insert("main", obj3) - if err != nil { - t.Fatalf("err: %v", err) - } - - // Delete a specific ID - num, err := txn.DeleteAll("main", "id", obj1.ID) - if err != nil { - t.Fatalf("err: %v", err) - } - if num != 1 { - t.Fatalf("Bad: %d", num) - } - - // Ensure we cannot lookup - raw, err := txn.First("main", "id", obj1.ID) - if err != nil { - t.Fatalf("err: %v", err) - } - if raw != nil { - t.Fatalf("bad: %#v", raw) - } - - // Delete an entire secondary range - num, err = txn.DeleteAll("main", "foo", obj2.Foo) - if err != nil { - t.Fatalf("err: %v", err) - } - if num != 2 { - t.Fatalf("Bad: %d", num) - } - - // Ensure we cannot lookup - raw, err = txn.First("main", "foo", obj2.Foo) - if err != nil { - t.Fatalf("err: %v", err) - } - if raw != nil { - t.Fatalf("bad: %#v", raw) - } -} - -func TestTxn_InsertGet_Prefix(t *testing.T) { - db := testDB(t) - txn := db.Txn(true) - - obj1 := &TestObject{ - ID: "my-cool-thing", - Foo: "foobarbaz", - } - obj2 := &TestObject{ - ID: "my-other-cool-thing", - Foo: "foozipzap", - } - - err := txn.Insert("main", obj1) - if err != nil { - t.Fatalf("err: %v", err) - } - err = txn.Insert("main", obj2) - if err != nil { - t.Fatalf("err: %v", err) - } - - checkResult := func(txn *Txn) { - // Attempt a row scan on the ID Prefix - result, err := txn.Get("main", "id_prefix") - if err != nil { - t.Fatalf("err: %v", err) - } - - if raw := result.Next(); raw != obj1 { - t.Fatalf("bad: %#v %#v", raw, obj1) - } - - if raw := result.Next(); raw != obj2 { - t.Fatalf("bad: %#v %#v", raw, obj2) - } - - if raw := result.Next(); raw != nil { - t.Fatalf("bad: %#v %#v", raw, nil) - } - - // Attempt a row scan on the ID with specific ID prefix - result, err = txn.Get("main", "id_prefix", "my-c") - if err != nil { - t.Fatalf("err: %v", err) - } - - if raw := result.Next(); raw != obj1 { - t.Fatalf("bad: %#v %#v", raw, obj1) - } - - if raw := result.Next(); raw != nil { - t.Fatalf("bad: %#v %#v", raw, nil) - } - - // Attempt a row scan secondary index - result, err = txn.Get("main", "foo_prefix", "foo") - if err != nil { - t.Fatalf("err: %v", err) - } - - if raw := result.Next(); raw != obj1 { - t.Fatalf("bad: %#v %#v", raw, obj1) - } - - if raw := result.Next(); raw != obj2 { - t.Fatalf("bad: %#v %#v", raw, obj2) - } - - if raw := result.Next(); raw != nil { - t.Fatalf("bad: %#v %#v", raw, nil) - } - - // Attempt a row scan secondary index, tigher prefix - result, err = txn.Get("main", "foo_prefix", "foob") - if err != nil { - t.Fatalf("err: %v", err) - } - - if raw := result.Next(); raw != obj1 { - t.Fatalf("bad: %#v %#v", raw, obj1) - } - - if raw := result.Next(); raw != nil { - t.Fatalf("bad: %#v %#v", raw, nil) - } - } - - // Check the results within the txn - checkResult(txn) - - // Commit and start a new read transaction - txn.Commit() - txn = db.Txn(false) - - // Check the results in a new txn - checkResult(txn) -}