Massive overhaul
This commit is contained in:
parent
435d1251ad
commit
73e2b6a7a6
@ -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.
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
18
bitcask/keeper_test.go
Normal file
18
bitcask/keeper_test.go
Normal file
@ -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(""))
|
||||
}
|
@ -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())
|
||||
}
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
7
filer.go
7
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
|
||||
}
|
||||
|
1
go.mod
1
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 (
|
||||
|
@ -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
|
||||
|
15
keyvalue.go
15
keyvalue.go
@ -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
|
||||
}
|
77
kv/keyvalue.go
Normal file
77
kv/keyvalue.go
Normal file
@ -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())
|
||||
}
|
61
kv/keyvalue_test.go
Normal file
61
kv/keyvalue_test.go
Normal file
@ -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())
|
||||
})
|
||||
}
|
12
searcher.go
12
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)
|
||||
}
|
||||
|
6
store.go
Normal file
6
store.go
Normal file
@ -0,0 +1,6 @@
|
||||
package database
|
||||
|
||||
type Store interface {
|
||||
Filer
|
||||
Searcher
|
||||
}
|
Loading…
Reference in New Issue
Block a user