Storage Index [WIP]

This commit is contained in:
Ilja Neumann
2020-10-14 18:03:52 +02:00
parent 74ef47562d
commit 85743a2129
10 changed files with 933 additions and 0 deletions
+65
View File
@@ -0,0 +1,65 @@
package index
import (
"encoding/json"
"io/ioutil"
"os"
"path"
"testing"
)
type User struct {
Id, UserName, Email string
}
type TestPet struct {
Id, Kind, Color, Name string
}
var testData = map[string][]interface{}{
"users": {
User{Id: "abcdefg-123", UserName: "mikey", Email: "mikey@example.com"},
User{Id: "hijklmn-456", UserName: "frank", Email: "frank@example.com"},
User{Id: "ewf4ofk-555", UserName: "jacky", Email: "jacky@example.com"},
User{Id: "rulan54-777", UserName: "jones", Email: "jones@example.com"},
},
"pets": {
TestPet{Id: "rebef-123", Kind: "Dog", Color: "Brown", Name: "Waldo"},
TestPet{Id: "wefwe-456", Kind: "Cat", Color: "White", Name: "Snowy"},
TestPet{Id: "goefe-789", Kind: "Hog", Color: "Green", Name: "Dicky"},
TestPet{Id: "xadaf-189", Kind: "Hog", Color: "Green", Name: "Ricky"},
},
}
func writeIndexTestData(t *testing.T, m map[string][]interface{}, pk string) string {
rootDir := createTmpDir(t)
for dirName := range m {
fileTypePath := path.Join(rootDir, dirName)
if err := os.MkdirAll(fileTypePath, 0777); err != nil {
t.Fatal(err)
}
for _, u := range m[dirName] {
data, err := json.Marshal(u)
if err != nil {
t.Fatal(err)
}
pkVal := valueOf(u, pk)
if err := ioutil.WriteFile(path.Join(fileTypePath, pkVal), data, 0777); err != nil {
t.Fatal(err)
}
}
}
return rootDir
}
func createTmpDir(t *testing.T) string {
name, err := ioutil.TempDir("/var/tmp", "testfiles-*")
if err != nil {
t.Fatal(err)
}
return name
}
+31
View File
@@ -0,0 +1,31 @@
package index
import (
"fmt"
)
type alreadyExistsErr struct {
typeName, key, val string
}
func (e *alreadyExistsErr) Error() string {
return fmt.Sprintf("%s with %s=%s does already exist", e.typeName, e.key, e.val)
}
func IsAlreadyExistsErr(e error) bool {
_, ok := e.(*alreadyExistsErr)
return ok
}
type notFoundErr struct {
typeName, key, val string
}
func (e *notFoundErr) Error() string {
return fmt.Sprintf("%s with %s=%s not found", e.typeName, e.key, e.val)
}
func IsNotFoundErr(e error) bool {
_, ok := e.(*notFoundErr)
return ok
}
+120
View File
@@ -0,0 +1,120 @@
// Package index provides symlink-based index for on-disk document-directories.
package index
import (
"github.com/rs/zerolog"
"path"
)
// Index is a facade to configure and query over multiple indices.
type Index struct {
config *Config
indices indexMap
}
type Config struct {
DataDir string
IndexRootDirName string
Log zerolog.Logger
}
// Type can be implemented to create new index-strategies. See Unique for example.
// Each index implementation is bound to one data-column (IndexBy) and a data-type (TypeName)
type Type interface {
Init() error
Lookup(v string) ([]string, error)
Add(id, v string) (string, error)
Remove(id string, v string) error
Update(id, oldV, newV string) error
Search(pattern string) ([]string, error)
IndexBy() string
TypeName() string
FilesDir() string
}
func NewIndex(cfg *Config) *Index {
return &Index{
config: cfg,
indices: indexMap{},
}
}
func (man Index) AddUniqueIndex(typeName, indexBy, entityDirName string) error {
fullDataPath := path.Join(man.config.DataDir, entityDirName)
indexPath := path.Join(man.config.DataDir, man.config.IndexRootDirName)
idx := NewUniqueIndex(typeName, indexBy, fullDataPath, indexPath)
man.indices.addIndex(idx)
return idx.Init()
}
func (man Index) AddNormalIndex(typeName, indexBy, entityDirName string) error {
fullDataPath := path.Join(man.config.DataDir, entityDirName)
indexPath := path.Join(man.config.DataDir, man.config.IndexRootDirName)
idx := NewNormalIndex(typeName, indexBy, fullDataPath, indexPath)
man.indices.addIndex(idx)
return idx.Init()
}
func (man Index) AddIndex(idx Type) error {
man.indices.addIndex(idx)
return idx.Init()
}
// Add a new entry to the index
func (man Index) Add(primaryKey string, entity interface{}) error {
t, err := getType(entity)
if err != nil {
return err
}
typeName := t.Type().Name()
if typeIndices, ok := man.indices[typeName]; ok {
for _, fieldIndices := range typeIndices {
for k := range fieldIndices {
curIdx := fieldIndices[k]
idxBy := curIdx.IndexBy()
val := valueOf(entity, idxBy)
_, err := curIdx.Add(primaryKey, val)
if err != nil {
return err
}
}
}
}
return nil
}
// Find a entry by type,field and value.
// // Find a User type by email
// man.Find("User", "Email", "foo@example.com")
func (man Index) Find(typeName, key, value string) (pk string, err error) {
var res = []string{}
if indices, ok := man.indices[typeName][key]; ok {
for _, idx := range indices {
if res, err = idx.Lookup(value); IsNotFoundErr(err) {
continue
}
if err != nil {
return
}
}
}
if len(res) == 0 {
return "", err
}
return path.Base(res[0]), err
}
func (man Index) Delete(typeName, pk string) error {
return nil
}
+89
View File
@@ -0,0 +1,89 @@
package index
import (
"fmt"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"os"
"testing"
)
func TestManagerQueryMultipleIndices(t *testing.T) {
dataDir := writeIndexTestData(t, testData, "Id")
man := NewIndex(&Config{
DataDir: dataDir,
IndexRootDirName: "index.disk",
Log: zerolog.Logger{},
})
err := man.AddUniqueIndex("User", "Email", "users")
assert.NoError(t, err)
err = man.AddUniqueIndex("User", "UserName", "users")
assert.NoError(t, err)
err = man.AddNormalIndex("TestPet", "Color", "pets")
assert.NoError(t, err)
err = man.AddUniqueIndex("TestPet", "Name", "pets")
assert.NoError(t, err)
for path := range testData {
for _, entity := range testData[path] {
err := man.Add(valueOf(entity, "Id"), entity)
assert.NoError(t, err)
}
}
type test struct {
typeName, key, value, wantRes string
wantErr error
}
tests := []test{
{typeName: "User", key: "Email", value: "jacky@example.com", wantRes: "ewf4ofk-555"},
{typeName: "User", key: "UserName", value: "jacky", wantRes: "ewf4ofk-555"},
{typeName: "TestPet", key: "Color", value: "Brown", wantRes: "rebef-123"},
{typeName: "TestPet", key: "Color", value: "Cyan", wantRes: "", wantErr: &notFoundErr{}},
}
for _, tc := range tests {
name := fmt.Sprintf("Query%sBy%s=%s", tc.typeName, tc.key, tc.value)
t.Run(name, func(t *testing.T) {
pk, err := man.Find(tc.typeName, tc.key, tc.value)
assert.Equal(t, tc.wantRes, pk)
assert.IsType(t, tc.wantErr, err)
})
}
_ = os.RemoveAll(dataDir)
}
func TestManagerDelete(t *testing.T) {
dataDir := writeIndexTestData(t, testData, "Id")
man := NewIndex(&Config{
DataDir: dataDir,
IndexRootDirName: "index.disk",
Log: zerolog.Logger{},
})
err := man.AddUniqueIndex("User", "Email", "users")
assert.NoError(t, err)
err = man.AddUniqueIndex("User", "UserName", "users")
assert.NoError(t, err)
err = man.AddUniqueIndex("TestPet", "Name", "pets")
assert.NoError(t, err)
for path := range testData {
for _, entity := range testData[path] {
err := man.Add(valueOf(entity, "Id"), entity)
assert.NoError(t, err)
}
}
err = man.Delete("User", "hijklmn-456")
_ = os.RemoveAll(dataDir)
}
+16
View File
@@ -0,0 +1,16 @@
package index
// indexMap stores the index layout at runtime.
type indexMap map[tName]map[indexByKey][]Type
type tName = string
type indexByKey = string
func (m indexMap) addIndex(idx Type) {
typeName, indexBy := idx.TypeName(), idx.IndexBy()
if _, ok := m[typeName]; !ok {
m[typeName] = map[indexByKey][]Type{}
}
m[typeName][indexBy] = append(m[typeName][indexBy], idx)
}
+170
View File
@@ -0,0 +1,170 @@
package index
import (
"errors"
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
)
// NonUniqueIndex is able to index an document by a key which might contain non-unique values
//
// /var/tmp/testfiles-395764020/index.disk/PetByColor/
// ├── Brown
// │ └── rebef-123 -> /var/tmp/testfiles-395764020/pets/rebef-123
// ├── Green
// │ ├── goefe-789 -> /var/tmp/testfiles-395764020/pets/goefe-789
// │ └── xadaf-189 -> /var/tmp/testfiles-395764020/pets/xadaf-189
// └── White
// └── wefwe-456 -> /var/tmp/testfiles-395764020/pets/wefwe-456
type NonUniqueIndex struct {
indexBy string
typeName string
filesDir string
indexBaseDir string
indexRootDir string
}
// NewNormalIndex instantiates a new NonUniqueIndex instance. Init() should be
// called afterward to ensure correct on-disk structure.
func NewNormalIndex(typeName, indexBy, filesDir, indexBaseDir string) NonUniqueIndex {
return NonUniqueIndex{
indexBy: indexBy,
typeName: typeName,
filesDir: filesDir,
indexBaseDir: indexBaseDir,
indexRootDir: path.Join(indexBaseDir, fmt.Sprintf("%sBy%s", typeName, indexBy)),
}
}
func (idx NonUniqueIndex) Init() error {
if _, err := os.Stat(idx.filesDir); err != nil {
return err
}
if err := os.MkdirAll(idx.indexRootDir, 0777); err != nil {
return err
}
return nil
}
func (idx NonUniqueIndex) Lookup(v string) ([]string, error) {
searchPath := path.Join(idx.indexRootDir, v)
fi, err := ioutil.ReadDir(searchPath)
if os.IsNotExist(err) {
return []string{}, &notFoundErr{idx.typeName, idx.indexBy, v}
}
if err != nil {
return []string{}, err
}
var ids []string = nil
for _, f := range fi {
ids = append(ids, f.Name())
}
if len(ids) == 0 {
return []string{}, &notFoundErr{idx.typeName, idx.indexBy, v}
}
return ids, nil
}
func (idx NonUniqueIndex) Add(id, v string) (string, error) {
oldName := path.Join(idx.filesDir, id)
newName := path.Join(idx.indexRootDir, v, id)
if err := os.MkdirAll(path.Join(idx.indexRootDir, v), 0777); err != nil {
return "", err
}
err := os.Symlink(oldName, newName)
if errors.Is(err, os.ErrExist) {
return "", &alreadyExistsErr{idx.typeName, idx.indexBy, v}
}
return newName, err
}
func (idx NonUniqueIndex) Remove(id string, v string) error {
res, err := filepath.Glob(path.Join(idx.indexRootDir, "/*/", id))
if err != nil {
return err
}
for _, p := range res {
if err := os.Remove(p); err != nil {
return err
}
}
return nil
}
func (idx NonUniqueIndex) Update(id, oldV, newV string) (err error) {
oldDir := path.Join(idx.indexRootDir, oldV)
oldPath := path.Join(oldDir, id)
newDir := path.Join(idx.indexRootDir, newV)
newPath := path.Join(newDir, id)
if _, err = os.Stat(oldPath); os.IsNotExist(err) {
return &notFoundErr{idx.typeName, idx.indexBy, oldV}
}
if err != nil {
return
}
if err = os.MkdirAll(newDir, 0777); err != nil {
return
}
if err = os.Rename(oldPath, newPath); err != nil {
return
}
di, err := ioutil.ReadDir(oldDir)
if err != nil {
return err
}
if len(di) == 0 {
err = os.RemoveAll(oldDir)
if err != nil {
return
}
}
return
}
func (idx NonUniqueIndex) Search(pattern string) ([]string, error) {
paths, err := filepath.Glob(path.Join(idx.indexRootDir, pattern, "*"))
if err != nil {
return nil, err
}
if len(paths) == 0 {
return nil, &notFoundErr{idx.typeName, idx.indexBy, pattern}
}
return paths, nil
}
func (idx NonUniqueIndex) IndexBy() string {
return idx.indexBy
}
func (idx NonUniqueIndex) TypeName() string {
return idx.typeName
}
func (idx NonUniqueIndex) FilesDir() string {
return idx.filesDir
}
+108
View File
@@ -0,0 +1,108 @@
package index
import (
"github.com/stretchr/testify/assert"
"os"
"path"
"testing"
)
func TestNonUniqueIndexAdd(t *testing.T) {
sut, dataPath := getNonUniqueIdxSut(t)
ids, err := sut.Lookup("Green")
assert.NoError(t, err)
assert.EqualValues(t, []string{"goefe-789", "xadaf-189"}, ids)
ids, err = sut.Lookup("White")
assert.NoError(t, err)
assert.EqualValues(t, []string{"wefwe-456"}, ids)
ids, err = sut.Lookup("Cyan")
assert.Error(t, err)
assert.EqualValues(t, []string{}, ids)
_ = os.RemoveAll(dataPath)
}
func TestNonUniqueIndexUpdate(t *testing.T) {
sut, dataPath := getNonUniqueIdxSut(t)
err := sut.Update("goefe-789", "Green", "Black")
assert.NoError(t, err)
err = sut.Update("xadaf-189", "Green", "Black")
assert.NoError(t, err)
assert.DirExists(t, path.Join(dataPath, "index.disk/PetByColor/Black"))
assert.NoDirExists(t, path.Join(dataPath, "index.disk/PetByColor/Green"))
_ = os.RemoveAll(dataPath)
}
func TestNonUniqueIndexDelete(t *testing.T) {
sut, dataPath := getNonUniqueIdxSut(t)
assert.FileExists(t, path.Join(dataPath, "index.disk/PetByColor/Green/goefe-789"))
err := sut.Remove("goefe-789", "")
assert.NoError(t, err)
assert.NoFileExists(t, path.Join(dataPath, "index.disk/PetByColor/Green/goefe-789"))
_ = os.RemoveAll(dataPath)
}
func TestNonUniqueIndexInit(t *testing.T) {
dataDir := createTmpDir(t)
indexRootDir := path.Join(dataDir, "index.disk")
filesDir := path.Join(dataDir, "users")
uniq := NewNormalIndex("User", "DisplayName", filesDir, indexRootDir)
assert.Error(t, uniq.Init(), "Init should return an error about missing files-dir")
if err := os.Mkdir(filesDir, 0777); err != nil {
t.Fatalf("Could not create test data-dir %s", err)
}
assert.NoError(t, uniq.Init(), "Init shouldn't return an error")
assert.DirExists(t, indexRootDir)
assert.DirExists(t, path.Join(indexRootDir, "UserByDisplayName"))
_ = os.RemoveAll(dataDir)
}
func TestNonUniqueIndexSearch(t *testing.T) {
sut, dataPath := getNonUniqueIdxSut(t)
res, err := sut.Search("Gr*")
assert.NoError(t, err)
assert.Len(t, res, 2)
assert.Equal(t, "goefe-789", path.Base(res[0]))
assert.Equal(t, "xadaf-189", path.Base(res[1]))
res, err = sut.Search("does-not-exist@example.com")
assert.Error(t, err)
assert.IsType(t, &notFoundErr{}, err)
_ = os.RemoveAll(dataPath)
}
func getNonUniqueIdxSut(t *testing.T) (sut Type, dataPath string) {
dataPath = writeIndexTestData(t, testData, "Id")
sut = NewNormalIndex("Pet", "Color", path.Join(dataPath, "pets"), path.Join(dataPath, "index.disk"))
err := sut.Init()
if err != nil {
t.Fatal(err)
}
for _, u := range testData["pets"] {
pkVal := valueOf(u, "Id")
idxByVal := valueOf(u, "Color")
_, err := sut.Add(pkVal, idxByVal)
if err != nil {
t.Fatal(err)
}
}
return
}
+25
View File
@@ -0,0 +1,25 @@
package index
import (
"errors"
"reflect"
)
func getType(v interface{}) (reflect.Value, error) {
rv := reflect.ValueOf(v)
for rv.Kind() == reflect.Ptr || rv.Kind() == reflect.Interface {
rv = rv.Elem()
}
if !rv.IsValid() {
return reflect.Value{}, errors.New("failed to read value via reflection")
}
return rv, nil
}
func valueOf(v interface{}, field string) string {
r := reflect.ValueOf(v)
f := reflect.Indirect(r).FieldByName(field)
return f.String()
}
+176
View File
@@ -0,0 +1,176 @@
package index
import (
"errors"
"fmt"
"os"
"path"
"path/filepath"
)
// Unique ensures that only one document of the same type and key-value combination can exist in the index.
//
// Modeled by creating a index-folder per entity and key with symlinks which point to respective documents which contain
// the link-filename as value.
//
// Directory Layout
//
// /var/data/index.disk/UniqueUserByEmail/
// ├── jacky@example.com -> /var/data/users/ewf4ofk-555
// ├── jones@example.com -> /var/data/users/rulan54-777
// └── mikey@example.com -> /var/data/users/abcdefg-123
//
// Example user
//
// {
// "Id": "ewf4ofk-555",
// "UserName": "jacky",
// "Email": "jacky@example.com"
// }
//
type Unique struct {
indexBy string
typeName string
filesDir string
indexBaseDir string
indexRootDir string
}
// NewUniqueIndex instantiates a new UniqueIndex instance. Init() should be
// called afterward to ensure correct on-disk structure.
func NewUniqueIndex(typeName, indexBy, filesDir, indexBaseDir string) Unique {
return Unique{
indexBy: indexBy,
typeName: typeName,
filesDir: filesDir,
indexBaseDir: indexBaseDir,
indexRootDir: path.Join(indexBaseDir, fmt.Sprintf("Unique%sBy%s", typeName, indexBy)),
}
}
func (idx Unique) Init() error {
if _, err := os.Stat(idx.filesDir); err != nil {
return err
}
if err := os.MkdirAll(idx.indexRootDir, 0777); err != nil {
return err
}
return nil
}
func (idx Unique) Add(id, v string) (string, error) {
oldName := path.Join(idx.filesDir, id)
newName := path.Join(idx.indexRootDir, v)
err := os.Symlink(oldName, newName)
if errors.Is(err, os.ErrExist) {
return "", &alreadyExistsErr{idx.typeName, idx.indexBy, v}
}
return newName, err
}
func (idx Unique) Remove(id string, v string) (err error) {
searchPath := path.Join(idx.indexRootDir, v)
if err = isValidSymlink(searchPath); err != nil {
return
}
return os.Remove(searchPath)
}
func (idx Unique) Lookup(v string) (resultPath []string, err error) {
searchPath := path.Join(idx.indexRootDir, v)
if err = isValidSymlink(searchPath); err != nil {
if os.IsNotExist(err) {
err = &notFoundErr{idx.typeName, idx.indexBy, v}
}
return
}
p, err := os.Readlink(searchPath)
if err != nil {
return []string{}, nil
}
return []string{p}, err
}
func (idx Unique) Update(id, oldV, newV string) (err error) {
oldPath := path.Join(idx.indexRootDir, oldV)
if err = isValidSymlink(oldPath); err != nil {
if os.IsNotExist(err) {
return &notFoundErr{idx.typeName, idx.indexBy, oldV}
}
return
}
newPath := path.Join(idx.indexRootDir, newV)
if err = isValidSymlink(newPath); err == nil {
return &alreadyExistsErr{idx.typeName, idx.indexBy, newV}
}
if os.IsNotExist(err) {
err = os.Rename(oldPath, newPath)
}
return
}
func (idx Unique) Search(pattern string) ([]string, error) {
paths, err := filepath.Glob(path.Join(idx.indexRootDir, pattern))
if err != nil {
return nil, err
}
if len(paths) == 0 {
return nil, &notFoundErr{idx.typeName, idx.indexBy, pattern}
}
res := make([]string, 0, 0)
for _, p := range paths {
if err := isValidSymlink(p); err != nil {
return nil, err
}
src, err := os.Readlink(p)
if err != nil {
return nil, err
}
res = append(res, src)
}
return res, nil
}
func (idx Unique) IndexBy() string {
return idx.indexBy
}
func (idx Unique) TypeName() string {
return idx.typeName
}
func (idx Unique) FilesDir() string {
return idx.filesDir
}
func isValidSymlink(path string) (err error) {
var symInfo os.FileInfo
if symInfo, err = os.Lstat(path); err != nil {
return
}
if symInfo.Mode()&os.ModeSymlink == 0 {
err = fmt.Errorf("%s is not a valid symlink (bug/corruption?)", path)
return
}
return
}
+133
View File
@@ -0,0 +1,133 @@
package index
import (
"github.com/stretchr/testify/assert"
"os"
"path"
"testing"
)
func TestUniqueLookupSingleEntry(t *testing.T) {
uniq, dataDir := getUniqueIdxSut(t)
filesDir := path.Join(dataDir, "users")
t.Log("existing lookup")
resultPath, err := uniq.Lookup("mikey@example.com")
assert.NoError(t, err)
assert.Equal(t, []string{path.Join(filesDir, "abcdefg-123")}, resultPath)
t.Log("non-existing lookup")
resultPath, err = uniq.Lookup("doesnotExists@example.com")
assert.Error(t, err)
assert.IsType(t, &notFoundErr{}, err)
assert.Empty(t, resultPath)
_ = os.RemoveAll(dataDir)
}
func TestUniqueUniqueConstraint(t *testing.T) {
uniq, dataDir := getUniqueIdxSut(t)
_, err := uniq.Add("abcdefg-123", "mikey@example.com")
assert.Error(t, err)
assert.IsType(t, &alreadyExistsErr{}, err)
_ = os.RemoveAll(dataDir)
}
func TestUniqueRemove(t *testing.T) {
uniq, dataDir := getUniqueIdxSut(t)
err := uniq.Remove("", "mikey@example.com")
assert.NoError(t, err)
_, err = uniq.Lookup("mikey@example.com")
assert.Error(t, err)
assert.IsType(t, &notFoundErr{}, err)
_ = os.RemoveAll(dataDir)
}
func TestUniqueUpdate(t *testing.T) {
uniq, dataDir := getUniqueIdxSut(t)
t.Log("successful update")
err := uniq.Update("", "mikey@example.com", "mikey2@example.com")
assert.NoError(t, err)
t.Log("failed update because already exists")
err = uniq.Update("", "frank@example.com", "mikey2@example.com")
assert.Error(t, err)
assert.IsType(t, &alreadyExistsErr{}, err)
t.Log("failed update because not found")
err = uniq.Update("", "notexist@example.com", "something2@example.com")
assert.Error(t, err)
assert.IsType(t, &notFoundErr{}, err)
_ = os.RemoveAll(dataDir)
}
func TestUniqueInit(t *testing.T) {
dataDir := createTmpDir(t)
indexRootDir := path.Join(dataDir, "index.disk")
filesDir := path.Join(dataDir, "users")
uniq := NewUniqueIndex("User", "Email", filesDir, indexRootDir)
assert.Error(t, uniq.Init(), "Init should return an error about missing files-dir")
if err := os.Mkdir(filesDir, 0777); err != nil {
t.Fatalf("Could not create test data-dir %s", err)
}
assert.NoError(t, uniq.Init(), "Init shouldn't return an error")
assert.DirExists(t, indexRootDir)
assert.DirExists(t, path.Join(indexRootDir, "UniqueUserByEmail"))
_ = os.RemoveAll(dataDir)
}
func TestUniqueIndexSearch(t *testing.T) {
sut, dataPath := getUniqueIdxSut(t)
res, err := sut.Search("j*@example.com")
assert.NoError(t, err)
assert.Len(t, res, 2)
assert.Equal(t, "ewf4ofk-555", path.Base(res[0]))
assert.Equal(t, "rulan54-777", path.Base(res[1]))
res, err = sut.Search("does-not-exist@example.com")
assert.Error(t, err)
assert.IsType(t, &notFoundErr{}, err)
_ = os.RemoveAll(dataPath)
}
func TestErrors(t *testing.T) {
assert.True(t, IsAlreadyExistsErr(&alreadyExistsErr{}))
assert.True(t, IsNotFoundErr(&notFoundErr{}))
}
func getUniqueIdxSut(t *testing.T) (sut Type, dataPath string) {
dataPath = writeIndexTestData(t, testData, "Id")
sut = NewUniqueIndex("User", "Email", path.Join(dataPath, "users"), path.Join(dataPath, "index.disk"))
err := sut.Init()
if err != nil {
t.Fatal(err)
}
for _, u := range testData["users"] {
pkVal := valueOf(u, "Id")
idxByVal := valueOf(u, "Email")
_, err := sut.Add(pkVal, idxByVal)
if err != nil {
t.Fatal(err)
}
}
return
}