Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
tauraamui committed Jul 5, 2023
0 parents commit a08c292
Show file tree
Hide file tree
Showing 9 changed files with 875 additions and 0 deletions.
30 changes: 30 additions & 0 deletions Makefile
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

71 changes: 71 additions & 0 deletions database.go
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()
}
231 changes: 231 additions & 0 deletions entry.go
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"),
}
}
63 changes: 63 additions & 0 deletions entry_internal_test.go
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"})
}
Loading

0 comments on commit a08c292

Please sign in to comment.