memdb: refactor into seperate repo

This commit is contained in:
Armon Dadgar 2015-06-16 16:06:32 -07:00
parent 7c4dce412e
commit caa6e7672d
9 changed files with 0 additions and 2104 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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