From 73e2b6a7a6018c232fdf573946f4e96643b10da5 Mon Sep 17 00:00:00 2001 From: "kayos@tcp.direct" Date: Mon, 25 Jul 2022 21:36:46 -0700 Subject: [PATCH] Massive overhaul --- bitcask/README.md | 8 +-- bitcask/bitcask.go | 37 ++++++++++-- bitcask/bitcask_search.go | 83 +++++++++++++++++---------- bitcask/bitcask_search_test.go | 102 +++++++++++++++++++++------------ bitcask/bitcask_test.go | 28 +++++++-- bitcask/keeper_test.go | 18 ++++++ bitcask/keyvalue.go | 54 ----------------- bitcask/keyvalue_test.go | 71 ----------------------- filer.go | 7 +++ go.mod | 1 + keeper.go | 9 +-- keyvalue.go | 15 ----- kv/keyvalue.go | 77 +++++++++++++++++++++++++ kv/keyvalue_test.go | 61 ++++++++++++++++++++ searcher.go | 12 ++-- store.go | 6 ++ 16 files changed, 356 insertions(+), 233 deletions(-) create mode 100644 bitcask/keeper_test.go delete mode 100644 bitcask/keyvalue.go delete mode 100644 bitcask/keyvalue_test.go delete mode 100644 keyvalue.go create mode 100644 kv/keyvalue.go create mode 100644 kv/keyvalue_test.go create mode 100644 store.go diff --git a/bitcask/README.md b/bitcask/README.md index ecf13ce..60ac7ca 100644 --- a/bitcask/README.md +++ b/bitcask/README.md @@ -132,14 +132,14 @@ Store is an implmentation of a Filer and a Searcher using Bitcask. #### func (Store) AllKeys ```go -func (c Store) AllKeys() (keys [][]byte) +func (s Store) AllKeys() (keys [][]byte) ``` AllKeys will return all keys in the database as a slice of byte slices. #### func (Store) PrefixScan ```go -func (c Store) PrefixScan(prefix string) ([]KeyValue, error) +func (s Store) PrefixScan(prefix string) ([]KeyValue, error) ``` PrefixScan will scan a Store for all keys that have a matching prefix of the given string and return a map of keys and values. (map[Key]Value) @@ -147,7 +147,7 @@ given string and return a map of keys and values. (map[Key]Value) #### func (Store) Search ```go -func (c Store) Search(query string) ([]KeyValue, error) +func (s Store) Search(query string) ([]KeyValue, error) ``` Search will search for a given string within all values inside of a Store. Note, type casting will be necessary. (e.g: []byte or string) @@ -155,7 +155,7 @@ type casting will be necessary. (e.g: []byte or string) #### func (Store) ValueExists ```go -func (c Store) ValueExists(value []byte) (key []byte, ok bool) +func (s Store) ValueExists(value []byte) (key []byte, ok bool) ``` ValueExists will check for the existence of a Value anywhere within the keyspace, returning the Key and true if found, or nil and false if not found. diff --git a/bitcask/bitcask.go b/bitcask/bitcask.go index 61e30b8..de18fc3 100644 --- a/bitcask/bitcask.go +++ b/bitcask/bitcask.go @@ -1,6 +1,7 @@ package bitcask import ( + "errors" "strings" "sync" @@ -13,6 +14,12 @@ import ( type Store struct { *bitcask.Bitcask database.Searcher + closed bool +} + +// Backend returns the underlying bitcask instance. +func (s Store) Backend() any { + return s.Bitcask } // DB is a mapper of a Filer and Searcher implementation using Bitcask. @@ -22,6 +29,17 @@ type DB struct { mu *sync.RWMutex } +// AllStores returns a list of all bitcask datastores. +func (db *DB) AllStores() []database.Filer { + db.mu.RLock() + defer db.mu.RUnlock() + var stores = make([]database.Filer, len(db.store)) + for _, s := range db.store { + stores = append(stores, s) + } + return stores +} + // OpenDB will either open an existing set of bitcask datastores at the given directory, or it will create a new one. func OpenDB(path string) *DB { return &DB{ @@ -59,7 +77,14 @@ func WithMaxValueSize(size uint64) bitcask.Option { } // Init opens a bitcask store at the given path to be referenced by storeName. -func (db *DB) Init(storeName string, bitcaskopts ...bitcask.Option) error { +func (db *DB) Init(storeName string, opts ...any) error { + var bitcaskopts []bitcask.Option + for _, opt := range opts { + if _, ok := opt.(bitcask.Option); !ok { + return errors.New("invalid bitcask option type") + } + bitcaskopts = append(bitcaskopts, opt.(bitcask.Option)) + } db.mu.Lock() defer db.mu.Unlock() @@ -83,18 +108,18 @@ func (db *DB) Init(storeName string, bitcaskopts ...bitcask.Option) error { } // With calls the given underlying bitcask instance. -func (db *DB) With(storeName string) Store { +func (db *DB) With(storeName string) database.Store { db.mu.RLock() defer db.mu.RUnlock() d, ok := db.store[storeName] if ok { return d } - return Store{Bitcask: nil} + return nil } // WithNew calls the given underlying bitcask instance, if it doesn't exist, it creates it. -func (db *DB) WithNew(storeName string) Store { +func (db *DB) WithNew(storeName string) database.Filer { db.mu.RLock() defer db.mu.RUnlock() d, ok := db.store[storeName] @@ -159,9 +184,9 @@ func (db *DB) withAll(action withAllAction) error { } switch action { case dclose: - err = namedErr(name, db.Close(name)) + err = namedErr(name, store.Close()) case dsync: - err = namedErr(name, db.Sync(name)) + err = namedErr(name, store.Sync()) default: return errUnknownAction } diff --git a/bitcask/bitcask_search.go b/bitcask/bitcask_search.go index c508f43..1fd1b5a 100644 --- a/bitcask/bitcask_search.go +++ b/bitcask/bitcask_search.go @@ -2,31 +2,42 @@ package bitcask import ( "strings" + + "git.tcp.direct/tcp.direct/database/kv" ) // Search will search for a given string within all values inside of a Store. // Note, type casting will be necessary. (e.g: []byte or string) -func (c Store) Search(query string) ([]KeyValue, error) { - var errs []error - var res []KeyValue - for _, key := range c.AllKeys() { - raw, _ := c.Get(key) - k := Key{b: key} - v := Value{b: raw} - if strings.Contains(string(raw), query) { - res = append(res, KeyValue{Key: k, Value: v}) +func (s Store) Search(query string) (<-chan *kv.KeyValue, chan error) { + var errChan = make(chan error) + var resChan = make(chan *kv.KeyValue, 5) + go func() { + defer func() { + close(resChan) + close(errChan) + }() + for _, key := range s.AllKeys() { + raw, err := s.Get(key) + if err != nil { + errChan <- err + continue + } + if raw != nil && strings.Contains(string(raw), query) { + keyVal := kv.NewKeyValue(kv.NewKey(key), kv.NewValue(raw)) + resChan <- keyVal + } } - } - return res, compoundErrors(errs) + }() + return resChan, errChan } // ValueExists will check for the existence of a Value anywhere within the keyspace, returning the Key and true if found, or nil and false if not found. -func (c Store) ValueExists(value []byte) (key []byte, ok bool) { +func (s Store) ValueExists(value []byte) (key []byte, ok bool) { var raw []byte - var needle = Value{b: value} - for _, k := range c.AllKeys() { - raw, _ = c.Get(k) - v := Value{b: raw} + var needle = kv.NewValue(value) + for _, k := range s.AllKeys() { + raw, _ = s.Get(k) + v := kv.NewValue(raw) if v.Equal(needle) { ok = true return @@ -37,22 +48,36 @@ func (c Store) ValueExists(value []byte) (key []byte, ok bool) { // PrefixScan will scan a Store for all keys that have a matching prefix of the given string // and return a map of keys and values. (map[Key]Value) -func (c Store) PrefixScan(prefix string) ([]KeyValue, error) { - var res []KeyValue - err := c.Scan([]byte(prefix), func(key []byte) error { - raw, _ := c.Get(key) - k := Key{b: key} - kv := KeyValue{Key: k, Value: Value{b: raw}} - res = append(res, kv) - - return nil - }) - return res, err +func (s Store) PrefixScan(prefix string) (<-chan *kv.KeyValue, chan error) { + errChan := make(chan error) + resChan := make(chan *kv.KeyValue, 5) + go func() { + var err error + defer func(e error) { + if e != nil { + errChan <- e + } + close(resChan) + close(errChan) + }(err) + err = s.Scan([]byte(prefix), func(key []byte) error { + raw, err := s.Get(key) + if err != nil { + return err + } + if key != nil && raw != nil { + k := kv.NewKey(key) + resChan <- kv.NewKeyValue(k, kv.NewValue(raw)) + } + return nil + }) + }() + return resChan, errChan } // AllKeys will return all keys in the database as a slice of byte slices. -func (c Store) AllKeys() (keys [][]byte) { - keychan := c.Keys() +func (s Store) AllKeys() (keys [][]byte) { + keychan := s.Keys() for key := range keychan { keys = append(keys, key) } diff --git a/bitcask/bitcask_search_test.go b/bitcask/bitcask_search_test.go index f13d32e..6651efa 100644 --- a/bitcask/bitcask_search_test.go +++ b/bitcask/bitcask_search_test.go @@ -8,6 +8,9 @@ import ( "testing" c "git.tcp.direct/kayos/common/entropy" + "github.com/davecgh/go-spew/spew" + + "git.tcp.direct/tcp.direct/database/kv" ) var needle = "yeet" @@ -108,25 +111,31 @@ func Test_Search(t *testing.T) { db.store["yeet"] = Store{Bitcask: nil} t.Run("BasicSearch", func(t *testing.T) { t.Logf("executing search for %s", needle) - - results, err := db.With(storename).Search(needle) - if err != nil { - t.Errorf("failed to search: %e", err) - } + resChan, errChan := db.With(storename).Search(needle) var keys = []int{one, two, three, four, five} var needed = len(keys) - for _, kv := range results { - keyint, err := strconv.Atoi(kv.Key.String()) - if err != nil { - t.Fatalf("failed to convert Key to int: %e", err) - } + + for keyValue := range resChan { + keyint, err := strconv.Atoi(keyValue.Key.String()) for _, k := range keys { if keyint == k { needed-- } } keys = append(keys, keyint) - t.Logf("Found Key: %s, Value: %s", kv.Key.String(), kv.Value.String()) + t.Logf("Found Key: %s, Value: %s", keyValue.Key.String(), keyValue.Value.String()) + + if err != nil { + t.Fatalf("failed to convert Key to int: %e", err) + } + select { + case err := <-errChan: + if err != nil { + t.Fatalf("failed to search: %e", err) + } + default: + continue + } } if needed != 0 { t.Errorf("Needed %d results, got %d", len(keys), len(keys)-needed) @@ -136,13 +145,19 @@ func Test_Search(t *testing.T) { t.Run("NoResultsSearch", func(t *testing.T) { bogus := c.RandStr(55) t.Logf("executing search for %s", bogus) - - results, err := db.With(storename).Search(bogus) - if err != nil { - t.Errorf("failed to search: %e", err) - } - if len(results) > 0 { - t.Errorf("[FAIL] got %d results, wanted 0", len(results)) + var results []*kv.KeyValue + resChan, errChan := db.With(storename).Search(bogus) + select { + case err := <-errChan: + t.Errorf("failed to search: %s", err.Error()) + case r := <-resChan: + if r != nil { + spew.Dump(r) + results = append(results, r) + } + if len(results) > 0 { + t.Errorf("[FAIL] got %d results, wanted 0", len(results)) + } } }) } @@ -189,40 +204,51 @@ func Test_PrefixScan(t *testing.T) { var storename = "test_prefix_scan" var db = setupTest(storename, t) addJunk(db, storename, c.RNG(5), c.RNG(5), c.RNG(5), c.RNG(5), c.RNG(5), t, false) - var needles = []KeyValue{ - {Key: Key{b: []byte("user:Frickhole")}, Value: Value{b: []byte(c.RandStr(55))}}, - {Key: Key{b: []byte("user:Johnson")}, Value: Value{b: []byte(c.RandStr(55))}}, - {Key: Key{b: []byte("user:Jackson")}, Value: Value{b: []byte(c.RandStr(55))}}, - {Key: Key{b: []byte("user:Frackhole")}, Value: Value{b: []byte(c.RandStr(55))}}, - {Key: Key{b: []byte("user:Baboshka")}, Value: Value{b: []byte(c.RandStr(55))}}, + var needles = []*kv.KeyValue{ + kv.NewKeyValue(kv.NewKey([]byte("user:Frickhole")), kv.NewValue([]byte(c.RandStr(55)))), + kv.NewKeyValue(kv.NewKey([]byte("user:Johnson")), kv.NewValue([]byte(c.RandStr(55)))), + kv.NewKeyValue(kv.NewKey([]byte("user:Jackson")), kv.NewValue([]byte(c.RandStr(55)))), + kv.NewKeyValue(kv.NewKey([]byte("user:Frackhole")), kv.NewValue([]byte(c.RandStr(55)))), + kv.NewKeyValue(kv.NewKey([]byte("user:Baboshka")), kv.NewValue([]byte(c.RandStr(55)))), } - for _, kv := range needles { - err := db.With(storename).Put(kv.Key.Bytes(), kv.Value.Bytes()) + for _, combo := range needles { + err := db.With(storename).Put(combo.Key.Bytes(), combo.Value.Bytes()) if err != nil { t.Fatalf("failed to add data to %s: %e", storename, err) } else { - t.Logf("added needle with key(value): %s(%s)", kv.Key.String(), kv.Value.String()) + t.Logf("added needle with key(value): %s(%s)", combo.Key.String(), combo.Value.String()) } } - res, err := db.With(storename).PrefixScan("user:") - if err != nil { - t.Errorf("failed to PrefixScan: %e", err) + resChan, errChan := db.With(storename).PrefixScan("user:") + var results []*kv.KeyValue + for keyValue := range resChan { + results = append(results, keyValue) + select { + case err := <-errChan: + if err != nil { + t.Fatalf("failed to PrefixScan: %e", err) + } + break + default: + continue + } } - if len(res) != len(needles) { - t.Errorf("[FAIL] Length of results (%d) is not the amount of needles we generated (%d)", len(res), len(needles)) + if len(results) != len(needles) { + t.Errorf("[FAIL] Length of results (%d) is not the amount of needles we generated (%d)", len(results), len(needles)) } var keysmatched = 0 - for _, kv := range res { + for _, result := range results { for _, ogkv := range needles { - if kv.Key.String() != ogkv.Key.String() { + if result.Key.String() != ogkv.Key.String() { continue } - t.Logf("[%s] Found needle key", ogkv.Key.String()) + t.Logf("Found needle key: %s", result.Key.String()) keysmatched++ - if kv.Value.String() != ogkv.Value.String() { - t.Errorf("[FAIL] values of key %s should have matched. wanted: %s, got: %s", kv.Key.String(), ogkv.Value.String(), kv.Value.String()) + if result.Value.String() != ogkv.Value.String() { + t.Errorf("[FAIL] values of key %s should have matched. wanted: %s, got: %s", + result.Key.String(), ogkv.Value.String(), result.Value.String()) } - t.Logf("[%s] Found needle value: %s", ogkv.Key.String(), ogkv.Value.String()) + t.Logf("Found needle value: %s", ogkv.Value.String()) } } if keysmatched != len(needles) { diff --git a/bitcask/bitcask_test.go b/bitcask/bitcask_test.go index 020326f..288711e 100644 --- a/bitcask/bitcask_test.go +++ b/bitcask/bitcask_test.go @@ -96,17 +96,35 @@ func TestDB_Init(t *testing.T) { //nolint:funlen,gocognit,cyclop } t.Logf("Got Value %v at Key %v", string(gvalue), key) }) - + t.Run("withNewStoreDoesExist", func(t *testing.T) { + nope := db.WithNew("bing") + if err := nope.Put([]byte("key"), []byte("value")); err != nil { + t.Fatalf("[FAIL] %e", err) + } + err := nope.Put([]byte("bing"), []byte("bong")) + if err != nil { + t.Fatalf("[FAIL] %e", err) + } + yup := db.WithNew("bing") + res, err := yup.Get([]byte("bing")) + if err != nil { + t.Errorf("[FAIL] %e", err) + } + if !bytes.Equal(res, []byte("bong")) { + t.Errorf("[FAIL] wanted %v, got %v", string([]byte("bong")), string(res)) + } + }) t.Run("withNewStoreDoesntExist", func(t *testing.T) { - if nope := db.WithNew("asdfqwerty"); nope.Bitcask == nil { + if nope := db.WithNew("asdfqwerty"); nope.Backend() == nil { t.Fatalf("[FAIL] got nil result for nonexistent store when it should have made itself: %T, %v", nope, nope) } else { t.Logf("[SUCCESS] got nil Value for store that doesn't exist") } }) t.Run("withStoreDoesntExist", func(t *testing.T) { - if nope := db.With("afsafdassdfqwerty"); nope.Bitcask != nil { - t.Fatalf("[FAIL] got non nil result for nonexistent store: %T, %v", nope, nope) + nope := db.With(c.RandStr(10)) + if nope != nil { + t.Fatalf("[FAIL] got non nil result for nonexistent store: %T, %v", nope.Backend(), nope.Backend()) } else { t.Logf("[SUCCESS] got nil Value for store that doesn't exist") } @@ -194,7 +212,7 @@ func Test_Close(t *testing.T) { }) t.Run("AssureClosed", func(t *testing.T) { for _, d := range oldstores { - if st := db.With(d); st.Bitcask != nil { + if st := db.With(d); st != nil { t.Fatalf("[FAIL] store %s should have been deleted", d) } } diff --git a/bitcask/keeper_test.go b/bitcask/keeper_test.go new file mode 100644 index 0000000..81e03c7 --- /dev/null +++ b/bitcask/keeper_test.go @@ -0,0 +1,18 @@ +package bitcask + +import ( + "testing" + + "git.tcp.direct/tcp.direct/database" +) + +func needKeeper(keeper database.Keeper) {} +func needFiler(filer database.Filer) {} + +func Test_Keeper(t *testing.T) { + needKeeper(OpenDB("")) +} + +func Test_Filer(t *testing.T) { + needFiler(OpenDB("").With("")) +} diff --git a/bitcask/keyvalue.go b/bitcask/keyvalue.go deleted file mode 100644 index 6bf2266..0000000 --- a/bitcask/keyvalue.go +++ /dev/null @@ -1,54 +0,0 @@ -package bitcask - -import ( - "bytes" - "git.tcp.direct/tcp.direct/database" -) - -// KeyValue represents a key and a value from a key/value store. -type KeyValue struct { - Key Key - Value Value -} - -// Key represents a key in a key/value store. -type Key struct { - database.Key - b []byte -} - -// Bytes returns the raw byte slice form of the Key. -func (k Key) Bytes() []byte { - return k.b -} - -// String returns the string slice form of the Key. -func (k Key) String() string { - return string(k.b) -} - -// Equal determines if two keys are equal. -func (k Key) Equal(k2 Key) bool { - return bytes.Equal(k.Bytes(), k2.Bytes()) -} - -// Value represents a value in a key/value store. -type Value struct { - database.Value - b []byte -} - -// Bytes returns the raw byte slice form of the Value. -func (v Value) Bytes() []byte { - return v.b -} - -// String returns the string slice form of the Value. -func (v Value) String() string { - return string(v.b) -} - -// Equal determines if two values are equal. -func (v Value) Equal(v2 Value) bool { - return bytes.Equal(v.Bytes(), v2.Bytes()) -} diff --git a/bitcask/keyvalue_test.go b/bitcask/keyvalue_test.go deleted file mode 100644 index 69565a0..0000000 --- a/bitcask/keyvalue_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package bitcask - -import ( - "testing" - - c "git.tcp.direct/kayos/common/entropy" -) - -func Test_Equal(t *testing.T) { - t.Run("ShouldBeEqual", func(t *testing.T) { - v := c.RandStr(55) - kv1 := KeyValue{Key{b: []byte(v)}, Value{b: []byte(v)}} - kv2 := KeyValue{Key{b: []byte(v)}, Value{b: []byte(v)}} - if !kv1.Key.Equal(kv2.Key) { - t.Errorf("[FAIL] Keys not equal: %s, %s", kv1.Key.String(), kv2.Key.String()) - } else { - if kv1.Key.String() == kv2.Key.String() { - t.Logf("[+] Keys are equal: %s", kv1.Key.String()) - } else { - t.Errorf( - "[FAIL] Equal() passed but strings are not the same! kv1: %s != kv2: %s", - kv1.Key.String(), kv2.Key.String(), - ) - } - } - - if !kv1.Value.Equal(kv2.Value) { - t.Errorf("[FAIL] Values not equal: %s, %s", kv1.Value.String(), kv2.Value.String()) - } else { - if kv1.Value.String() == kv2.Value.String() { - t.Logf("[+] Values are equal: %s", kv1.Value.String()) - } else { - t.Errorf( - "[FAIL] Equal() passed but strings are not the same! kv1: %s != kv2: %s", - kv1.Value.String(), kv2.Value.String(), - ) - } - } - }) - t.Run("ShouldNotBeEqual", func(t *testing.T) { - v1 := c.RandStr(55) - v2 := c.RandStr(55) - kv1 := KeyValue{Key{b: []byte(v1)}, Value{b: []byte(v1)}} - kv2 := KeyValue{Key{b: []byte(v2)}, Value{b: []byte(v2)}} - if kv1.Key.Equal(kv2.Key) { - t.Errorf("[FAIL] Keys are equal: %s, %s", kv1.Key.String(), kv2.Key.String()) - } else { - if kv1.Key.String() != kv2.Key.String() { - t.Logf("[+] Keys are not equal: %s", kv1.Key.String()) - } else { - t.Errorf( - "[FAIL] Equal() passed but strings are the same! kv1: %s != kv2: %s", - kv1.Key.String(), kv2.Key.String(), - ) - } - } - - if kv1.Value.Equal(kv2.Value) { - t.Errorf("[FAIL] Values are equal: %s, %s", kv1.Value.String(), kv2.Value.String()) - } else { - if kv1.Value.String() != kv2.Value.String() { - t.Logf("[+] Values are not equal: %s", kv1.Value.String()) - } else { - t.Errorf( - "[FAIL] Equal() passed but strings are the same! kv1: %s != kv2: %s", - kv1.Value.String(), kv2.Value.String(), - ) - } - } - }) -} diff --git a/filer.go b/filer.go index e50999e..55f3eb0 100644 --- a/filer.go +++ b/filer.go @@ -10,6 +10,9 @@ type Filer interface { // NOTE: One can easily cast anything to a byte slice. (e.g: []byte("fuckholejones") ) // json.Marshal also returns a byte slice by default ;) + // Backend returns the underlying key/value store. + Backend() any + // Has should return true if the given key has an associated value. Has(key []byte) bool // Get should retrieve the byte slice corresponding to the given key, and any associated errors upon failure. @@ -18,4 +21,8 @@ type Filer interface { Put(key []byte, value []byte) error // Delete should delete the key and the value associated with the given key, and return an error upon failure. Delete(key []byte) error + // Close should safely end any Filer operations of the given dataStore and close any relevant handlers. + Close() error + // Sync should take any volatile data and solidify it somehow if relevant. (ram to disk in most cases) + Sync() error } diff --git a/go.mod b/go.mod index 0cbc500..f8b0ac3 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.18 require ( git.tcp.direct/Mirrors/bitcask-mirror v0.0.0-20220228092422-1ec4297c7e34 git.tcp.direct/kayos/common v0.7.0 + github.com/davecgh/go-spew v1.1.1 ) require ( diff --git a/keeper.go b/keeper.go index f7d7d24..daa7c40 100644 --- a/keeper.go +++ b/keeper.go @@ -6,14 +6,11 @@ type Keeper interface { // Path should return the base path where all stores should be stored under. (likely as subdirectories) Path() string // Init should initialize our Filer at the given path, to be referenced and called by dataStore. - Init(dataStore []byte) error + Init(name string, options ...any) error // With provides access to the given dataStore by providing a pointer to the related Filer. - With(dataStore []byte) Filer - // Close should safely end any Filer operations of the given dataStore and close any relevant handlers. - Close(dataStore []byte) error - // Sync should take any volatile data and solidify it somehow if relevant. (ram to disk in most cases) - Sync(dataStore []byte) error + With(name string) Store + AllStores() []Filer // TODO: Backups CloseAll() error diff --git a/keyvalue.go b/keyvalue.go deleted file mode 100644 index 1db0b8d..0000000 --- a/keyvalue.go +++ /dev/null @@ -1,15 +0,0 @@ -package database - -// Key represents a key in a key/value Filer. -type Key interface { - Bytes() []byte - String() string - Equal(Key) bool -} - -// Value represents a value in a key/value Filer. -type Value interface { - Bytes() []byte - String() string - Equal(Value) bool -} diff --git a/kv/keyvalue.go b/kv/keyvalue.go new file mode 100644 index 0000000..32119a7 --- /dev/null +++ b/kv/keyvalue.go @@ -0,0 +1,77 @@ +package kv + +import ( + "bytes" +) + +// KeyValue represents a key and a value from a key/value store. +type KeyValue struct { + Key *Key + Value *Value +} + +// Key represents a key in a key/value store. +type Key struct { + b []byte +} + +// NewKey creates a new Key from a byte slice. +func NewKey(data []byte) *Key { + k := Key{b: data} + return &k +} + +// NewValue creates a new Value from a byte slice. +func NewValue(data []byte) *Value { + v := Value{b: data} + return &v +} + +// NewKeyValue creates a new KeyValue from a key and value. +func NewKeyValue(k *Key, v *Value) *KeyValue { + return &KeyValue{Key: k, Value: v} +} + +func (kv *KeyValue) String() string { + return kv.Key.String() + ":" + kv.Value.String() +} + +// Equal determines if two key/value pairs are equal. +func (kv *KeyValue) Equal(kv2 *KeyValue) bool { + return kv.Key.Equal(kv2.Key) && kv.Value.Equal(kv2.Value) +} + +// Bytes returns the raw byte slice form of the Key. +func (k *Key) Bytes() []byte { + return k.b +} + +// String returns the string slice form of the Key. +func (k *Key) String() string { + return string(k.b) +} + +// Equal determines if two keys are equal. +func (k *Key) Equal(k2 *Key) bool { + return bytes.Equal(k.Bytes(), k2.Bytes()) +} + +// Value represents a value in a key/value store. +type Value struct { + b []byte +} + +// Bytes returns the raw byte slice form of the Value. +func (v *Value) Bytes() []byte { + return v.b +} + +// String returns the string slice form of the Value. +func (v *Value) String() string { + return string(v.b) +} + +// Equal determines if two values are equal. +func (v *Value) Equal(v2 *Value) bool { + return bytes.Equal(v.Bytes(), v2.Bytes()) +} diff --git a/kv/keyvalue_test.go b/kv/keyvalue_test.go new file mode 100644 index 0000000..6019f6f --- /dev/null +++ b/kv/keyvalue_test.go @@ -0,0 +1,61 @@ +package kv + +import ( + "testing" + + c "git.tcp.direct/kayos/common/entropy" +) + +func Test_Equal(t *testing.T) { + t.Run("ShouldBeEqual", func(t *testing.T) { + v := c.RandStr(55) + kv1 := NewKeyValue(NewKey([]byte(v)), NewValue([]byte(v))) + kv2 := NewKeyValue(NewKey([]byte(v)), NewValue([]byte(v))) + if !kv1.Key.Equal(kv2.Key) { + t.Fatalf("[FAIL] Keys not equal: %s, %s", kv1.Key.String(), kv2.Key.String()) + } + if kv1.Key.String() != kv2.Key.String() { + t.Fatalf( + "[FAIL] Equal() passed but strings are not the same! kv1: %s != kv2: %s", + kv1.Key.String(), kv2.Key.String()) + } + if !kv1.Equal(kv2) { + t.Fatal("[FAIL] KeyValue.Equal failed") + } + t.Logf("[+] KeyValues are equal: %s == %s", kv1.String(), kv2.String()) + + if !kv1.Value.Equal(kv2.Value) { + t.Fatalf("[FAIL] Values not equal: %s, %s", kv1.Value.String(), kv2.Value.String()) + } + if kv1.Value.String() == kv2.Value.String() { + t.Logf("[+] Values are equal: %s", kv1.Value.String()) + } else { + t.Errorf( + "[FAIL] Equal() passed but strings are not the same! kv1: %s != kv2: %s", + kv1.Value.String(), kv2.Value.String(), + ) + } + }) + t.Run("ShouldNotBeEqual", func(t *testing.T) { + v1 := c.RandStr(55) + v2 := c.RandStr(55) + kv1 := NewKeyValue(NewKey([]byte(v1)), NewValue([]byte(v1))) + kv2 := NewKeyValue(NewKey([]byte(v2)), NewValue([]byte(v2))) + if kv1.Key.Equal(kv2.Key) { + t.Fatalf("[FAIL] Keys are equal: %s, %s", kv1.Key.String(), kv2.Key.String()) + } + if kv1.Key.String() == kv2.Key.String() { + t.Fatalf("[FAIL] Equal() did not pass but strings are the same! kv1: %s == kv2: %s", + kv1.Key.String(), kv2.Key.String()) + } + t.Logf("[+] Keys are not equal: %s", kv1.Key.String()) + if kv1.Value.Equal(kv2.Value) { + t.Fatalf("[FAIL] Values are equal: %s, %s", kv1.Value.String(), kv2.Value.String()) + } + if kv1.Value.String() == kv2.Value.String() { + t.Fatalf("[FAIL] Equal() passed but strings are the same! kv1: %s != kv2: %s", + kv1.Value.String(), kv2.Value.String()) + } + t.Logf("[+] Values are not equal: %s", kv1.Value.String()) + }) +} diff --git a/searcher.go b/searcher.go index fad1895..3800756 100644 --- a/searcher.go +++ b/searcher.go @@ -1,13 +1,15 @@ package database +import "git.tcp.direct/tcp.direct/database/kv" + // Searcher must be able to search through our datastore(s) with strings. type Searcher interface { // AllKeys must retrieve all keys in the datastore with the given storeName. - AllKeys() []string - // PrefixScan must return all keys that begin with the given prefix. - PrefixScan(prefix string) map[string]interface{} - // Search must be able to search through the contents of our database and return a map of results. - Search(query string) map[string]interface{} + AllKeys() [][]byte + // PrefixScan must retrieve all keys in the datastore and stream them to the given channel. + PrefixScan(prefix string) (<-chan *kv.KeyValue, chan error) + // Search must be able to search through the value contents of our database and stream the results to the given channel. + Search(query string) (<-chan *kv.KeyValue, chan error) // ValueExists searches for an exact match of the given value and returns the key that contains it. ValueExists(value []byte) (key []byte, ok bool) } diff --git a/store.go b/store.go new file mode 100644 index 0000000..9f4e8f5 --- /dev/null +++ b/store.go @@ -0,0 +1,6 @@ +package database + +type Store interface { + Filer + Searcher +}