-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit a08c292
Showing
9 changed files
with
875 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
.DEFAULT_GOAL := default | ||
|
||
.PHONY: test | ||
test: | ||
gotestsum ./... | ||
|
||
.PHONY: test-verbose | ||
test-verbose: | ||
gotestsum --format standard-verbose ./... | ||
|
||
.PHONY: coverage | ||
coverage: | ||
go test -coverpkg=./... -coverprofile=coverage.out ./... && go tool cover -func coverage.out && rm coverage.out | ||
|
||
.PHONY: coverage-persist | ||
coverage-persist: | ||
go test -coverpkg=./... -coverprofile=coverage.out ./... && go tool cover -func coverage.out | ||
|
||
.PHONY: install-gotestsum | ||
install-gotestsum: | ||
go get github.com/gotestyourself/gotestsum | ||
|
||
.PHONY: install-linter | ||
install-linter: | ||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.41.1 | ||
|
||
.PHONY: lint | ||
lint: | ||
golangci-lint run | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
package kvs | ||
|
||
import ( | ||
"fmt" | ||
"io" | ||
"os" | ||
|
||
"github.com/dgraph-io/badger/v3" | ||
) | ||
|
||
type DB struct { | ||
conn *badger.DB | ||
} | ||
|
||
func NewDB(db *badger.DB) (DB, error) { | ||
return newDB(false) | ||
} | ||
|
||
func NewMemDB() (DB, error) { | ||
return newDB(true) | ||
} | ||
|
||
func newDB(inMemory bool) (DB, error) { | ||
db, err := badger.Open(badger.DefaultOptions("").WithLogger(nil).WithInMemory(inMemory)) | ||
if err != nil { | ||
return DB{}, err | ||
} | ||
|
||
return DB{conn: db}, nil | ||
} | ||
|
||
func (db DB) GetSeq(key []byte, bandwidth uint64) (*badger.Sequence, error) { | ||
return db.conn.GetSequence(key, bandwidth) | ||
} | ||
|
||
func (db DB) View(f func(txn *badger.Txn) error) error { | ||
return db.conn.View(f) | ||
} | ||
|
||
func (db DB) Update(f func(txn *badger.Txn) error) error { | ||
return db.conn.Update(f) | ||
} | ||
|
||
func (db DB) DumpTo(w io.Writer) error { | ||
return db.conn.View(func(txn *badger.Txn) error { | ||
opts := badger.DefaultIteratorOptions | ||
opts.PrefetchSize = 10 | ||
it := txn.NewIterator(opts) | ||
defer it.Close() | ||
for it.Rewind(); it.Valid(); it.Next() { | ||
item := it.Item() | ||
k := item.Key() | ||
err := item.Value(func(v []byte) error { | ||
fmt.Fprintf(w, "key=%s, value=%s\n", k, v) | ||
return nil | ||
}) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
}) | ||
} | ||
|
||
func (db DB) DumpToStdout() error { | ||
return db.DumpTo(os.Stdout) | ||
} | ||
|
||
func (db DB) Close() error { | ||
return db.conn.Close() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,231 @@ | ||
package kvs | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"reflect" | ||
"strconv" | ||
"strings" | ||
|
||
"github.com/dgraph-io/badger/v3" | ||
"github.com/google/uuid" | ||
) | ||
|
||
type Entry struct { | ||
TableName string | ||
ColumnName string | ||
OwnerID uint32 | ||
OwnerUUID UUID | ||
RowID uint32 | ||
Data []byte | ||
} | ||
|
||
func (e Entry) PrefixKey() []byte { | ||
return []byte(fmt.Sprintf("%s.%s.%s", e.TableName, e.ColumnName, e.resolveOwnerID())) | ||
} | ||
|
||
func (e Entry) Key() []byte { | ||
return []byte(fmt.Sprintf("%s.%s.%s.%d", e.TableName, e.ColumnName, e.resolveOwnerID(), e.RowID)) | ||
} | ||
|
||
func (e Entry) resolveOwnerID() string { | ||
if e.OwnerUUID != nil { | ||
if ustr := e.OwnerUUID.String(); len(ustr) != 0 { | ||
return ustr | ||
} | ||
} | ||
|
||
return strconv.Itoa(int(e.OwnerID)) | ||
} | ||
|
||
func Store(db DB, e Entry) error { | ||
return db.conn.Update(func(txn *badger.Txn) error { | ||
return txn.Set([]byte(e.Key()), e.Data) | ||
}) | ||
} | ||
|
||
func Get(db DB, e *Entry) error { | ||
return db.conn.View(func(txn *badger.Txn) error { | ||
lookupKey := e.Key() | ||
item, err := txn.Get(lookupKey) | ||
if err != nil { | ||
return fmt.Errorf("%s: %s", strings.ToLower(err.Error()), lookupKey) | ||
} | ||
|
||
if err := item.Value(func(val []byte) error { | ||
e.Data = val | ||
return nil | ||
}); err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
}) | ||
} | ||
|
||
func ConvertToBlankEntries(tableName string, ownerID, rowID uint32, x interface{}) []Entry { | ||
v := reflect.ValueOf(x) | ||
return convertToEntries(tableName, ownerID, rowID, v, false) | ||
} | ||
|
||
func ConvertToEntries(tableName string, ownerID, rowID uint32, x interface{}) []Entry { | ||
v := reflect.ValueOf(x) | ||
return convertToEntries(tableName, ownerID, rowID, v, true) | ||
} | ||
|
||
type UUID interface { | ||
String() string | ||
} | ||
|
||
type RootOwner struct{} | ||
|
||
func (o RootOwner) String() string { return "root" } | ||
|
||
func ConvertToBlankEntriesWithUUID(tableName string, ownerID UUID, rowID uint32, x interface{}) []Entry { | ||
v := reflect.ValueOf(x) | ||
return convertToEntriesWithUUID(tableName, 0, rowID, ownerID, v, false) | ||
} | ||
|
||
func ConvertToEntriesWithUUID(tableName string, ownerID UUID, rowID uint32, x interface{}) []Entry { | ||
v := reflect.ValueOf(x) | ||
return convertToEntriesWithUUID(tableName, 0, rowID, ownerID, v, true) | ||
} | ||
|
||
func LoadEntry(s interface{}, entry Entry) error { | ||
// convert the interface value to a reflect.Value so we can access its fields | ||
val := reflect.ValueOf(s).Elem() | ||
|
||
field, err := resolveFieldRef(val, entry.ColumnName) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// convert the entry's Data field to the type of the target field | ||
if err := convertFromBytes(entry.Data, field.Addr().Interface()); err != nil { | ||
return fmt.Errorf("failed to convert entry data to field type: %v", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func resolveFieldRef(v reflect.Value, nameToMatch string) (reflect.Value, error) { | ||
t := v.Type() | ||
|
||
for i := 0; i < t.NumField(); i++ { | ||
field := t.Field(i) | ||
|
||
if strings.EqualFold(field.Name, nameToMatch) { | ||
return v.Field(i), nil | ||
} | ||
} | ||
|
||
return reflect.Zero(reflect.TypeOf(v)), fmt.Errorf("struct does not have a field with name %q", nameToMatch) | ||
} | ||
|
||
func LoadEntries(s interface{}, entries []Entry) error { | ||
for _, entry := range entries { | ||
if err := LoadEntry(s, entry); err != nil { | ||
return err | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func convertToEntriesWithUUID(tableName string, ownerID, rowID uint32, ownerUUID UUID, v reflect.Value, includeData bool) []Entry { | ||
entries := []Entry{} | ||
|
||
if v.Kind() == reflect.Pointer { | ||
v = v.Elem() | ||
} | ||
for i := 0; i < v.NumField(); i++ { | ||
vv := reflect.Indirect(v) | ||
f := vv.Type().Field(i) | ||
|
||
fOpts := resolveFieldOptions(f) | ||
if fOpts.Ignore { | ||
continue | ||
} | ||
|
||
e := Entry{ | ||
TableName: tableName, | ||
ColumnName: strings.ToLower(f.Name), | ||
OwnerID: ownerID, | ||
OwnerUUID: ownerUUID, | ||
RowID: rowID, | ||
} | ||
|
||
if includeData { | ||
bd, err := convertToBytes(v.Field(i).Interface()) | ||
if err != nil { | ||
return entries | ||
} | ||
e.Data = bd | ||
} | ||
|
||
entries = append(entries, e) | ||
} | ||
|
||
return entries | ||
} | ||
|
||
// TODO:(tauraamui): come up with novel and well designed method of preserving full upper casing | ||
func convertToEntries(tableName string, ownerID, rowID uint32, v reflect.Value, includeData bool) []Entry { | ||
return convertToEntriesWithUUID(tableName, ownerID, rowID, uuid.UUID{}, v, includeData) | ||
} | ||
|
||
func convertToBytes(i interface{}) ([]byte, error) { | ||
// Check the type of the interface. | ||
switch v := i.(type) { | ||
case []byte: | ||
// Return the input as a []byte if it is already a []byte. | ||
return v, nil | ||
case string: | ||
// Convert the string to a []byte and return it. | ||
return []byte(v), nil | ||
default: | ||
// Use json.Marshal to convert the interface to a []byte. | ||
return json.Marshal(v) | ||
} | ||
} | ||
|
||
func convertFromBytes(data []byte, i interface{}) error { | ||
// Check that the destination argument is a pointer. | ||
if reflect.TypeOf(i).Kind() != reflect.Ptr { | ||
return fmt.Errorf("destination must be a pointer") | ||
} | ||
|
||
// Check the type of the interface. | ||
switch v := i.(type) { | ||
case *[]byte: | ||
// Set the value of the interface to the []byte if it is a pointer to a []byte. | ||
*v = data | ||
return nil | ||
case *string: | ||
// Convert the []byte to a string and set the value of the interface to the string. | ||
*v = string(data) | ||
return nil | ||
case *UUID: | ||
// Convert the []byte to a UUID instance and set the value of the interface to it. | ||
uuidv, err := uuid.ParseBytes(data) | ||
if err != nil { | ||
return err | ||
} | ||
*v = uuidv | ||
return nil | ||
default: | ||
// Use json.Unmarshal to convert the []byte to the interface. | ||
return json.Unmarshal(data, v) | ||
} | ||
} | ||
|
||
type mdbFieldOptions struct { | ||
Ignore bool | ||
} | ||
|
||
func resolveFieldOptions(f reflect.StructField) mdbFieldOptions { | ||
mdbTagValue := f.Tag.Get("mdb") | ||
return mdbFieldOptions{ | ||
Ignore: strings.Contains(mdbTagValue, "ignore"), | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
package kvs | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/matryer/is" | ||
) | ||
|
||
func TestConvertToBytes(t *testing.T) { | ||
is := is.New(t) | ||
|
||
// Test input and expected output. | ||
tests := []struct { | ||
input interface{} | ||
expected []byte | ||
}{ | ||
{[]byte{1, 2, 3}, []byte{1, 2, 3}}, | ||
{"hello", []byte("hello")}, | ||
{struct{ A int }{5}, []byte("{\"A\":5}")}, | ||
} | ||
|
||
// Iterate over the tests and compare the output of convertToBytes to the expected output. | ||
for _, test := range tests { | ||
result, err := convertToBytes(test.input) | ||
is.NoErr(err) | ||
is.Equal(result, test.expected) | ||
} | ||
} | ||
|
||
func TestConvertBytesFromBytes(t *testing.T) { | ||
is := is.New(t) | ||
|
||
input := []byte{1, 2, 3} | ||
var destination []byte | ||
err := convertFromBytes(input, &destination) | ||
is.NoErr(err) | ||
is.Equal(destination, input) | ||
|
||
} | ||
|
||
func TestConvertStringFromBytes(t *testing.T) { | ||
is := is.New(t) | ||
|
||
input := []byte("hello") | ||
var destination string | ||
err := convertFromBytes(input, &destination) | ||
is.NoErr(err) | ||
is.Equal(destination, string(input)) | ||
} | ||
|
||
func TestConvertStructFromBytes(t *testing.T) { | ||
is := is.New(t) | ||
|
||
type TestStruct struct { | ||
A int | ||
B string | ||
} | ||
input := []byte("{\"A\":5,\"B\":\"hello\"}") | ||
var destination TestStruct | ||
err := convertFromBytes(input, &destination) | ||
is.NoErr(err) | ||
is.Equal(destination, TestStruct{A: 5, B: "hello"}) | ||
} |
Oops, something went wrong.