Last active 1699801710

Trying to add support for Collection.List() in Bitcask

Revision 0a7bee8ac4badfaf7258f612db3837b40b05321f

collection.go Raw
1package bitcask
2
3import (
4 "encoding/json"
5 "errors"
6 "fmt"
7 "reflect"
8)
9
10var (
11 // ErrObjectNotFound is the error returned when an object is not found in the collection.
12 ErrObjectNotFound = errors.New("error: object not found")
13)
14
15func (b *bitcask) Collection(name string) *Collection {
16 return &Collection{db: b, name: name}
17}
18
19// Collection allows you to manage a collection of objects encoded as JSON
20// documents with a path-based key based on the provided name. This is convenient
21// for storing complex normalized collections of objects.
22type Collection struct {
23 db DB
24 name string
25}
26
27func (c *Collection) makeKey(id string) Key {
28 return Key(fmt.Sprintf("%s/%s", c.name, id))
29}
30
31func (c *Collection) makePrefix() Key {
32 return Key(c.name)
33}
34
35// Add adds a new object to the collection
36func (c *Collection) Add(id string, obj any) error {
37 k := c.makeKey(id)
38 v, err := json.Marshal(obj)
39 if err != nil {
40 return err
41 }
42
43 return c.db.Put(k, v)
44}
45
46// Delete deletes an object from the collection
47func (c *Collection) Delete(id string) error {
48 return c.db.Delete(c.makeKey(id))
49}
50
51// Get returns an object from the collection
52func (c *Collection) Get(id string, obj any) error {
53 k := c.makeKey(id)
54 data, err := c.db.Get(k)
55 if err != nil {
56 if err == ErrKeyNotFound {
57 return ErrObjectNotFound
58 }
59 return err
60 }
61
62 return json.Unmarshal(data, obj)
63}
64
65// List returns a list of all objects in this collection.
66func (c *Collection) List(objects any) error {
67 tx := c.db.Transaction()
68 defer tx.Discard()
69
70 value := reflect.ValueOf(objects).Elem()
71 elem := reflect.TypeOf(objects).Elem()
72
73 return tx.Scan(c.makePrefix(), func(k Key) error {
74 data, err := tx.Get(k)
75 if err != nil {
76 return err
77 }
78
79 obj := reflect.New(elem)
80
81 if err := json.Unmarshal(data, &obj); err != nil {
82 return err
83 }
84
85 value.Set(reflect.Append(value, reflect.ValueOf(obj)))
86
87 return nil
88 })
89}
90
91// Has returns true if an object exists by the provided id.
92func (c *Collection) Has(id string) bool {
93 return c.db.Has(c.makeKey(id))
94}
95
96// Count returns the number of objects in this collection.
97func (c *Collection) Count() int {
98 n := 0
99 c.db.Scan(c.makePrefix(), func(k Key) error {
100 n++
101 return nil
102 })
103 return n
104}
105
106// Drop deletes the entire collection
107func (c *Collection) Drop() error {
108 tx := c.db.Transaction()
109 defer tx.Discard()
110
111 if err := tx.Scan(c.makePrefix(), func(k Key) error {
112 return tx.Delete(k)
113 }); err != nil {
114 return err
115 }
116
117 return tx.Commit()
118}
119
120// Exists returns true if the collection exists at all, which really just means
121// whether there are any objects in the collection, so this is the same as calling
122// Count() > 0.
123func (c *Collection) Exists() bool {
124 return c.Count() > 0
125}
collection_test.go Raw
1package bitcask
2
3import (
4 "os"
5 "testing"
6
7 "github.com/stretchr/testify/assert"
8 "github.com/stretchr/testify/require"
9)
10
11func setupDB(t *testing.T) DB {
12 t.Helper()
13
14 testDir, err := os.MkdirTemp("", "bitcask")
15 assert.NoError(t, err)
16
17 db, err := Open(testDir)
18 assert.NoError(t, err)
19
20 return db
21}
22
23func TestCollection(t *testing.T) {
24 type User struct {
25 Name string
26 Age int
27 }
28
29 t.Run("AddGetDelete", func(t *testing.T) {
30 db := setupDB(t)
31 defer db.Close()
32 c := db.Collection("users")
33
34 err := c.Add("prologic", &User{"James", 21})
35 assert.NoError(t, err)
36
37 var actual User
38 expected := User{"James", 21}
39 require.NoError(t, c.Get("prologic", &actual))
40 assert.EqualValues(t, expected, actual)
41
42 actual = User{}
43 expected = User{}
44 require.NoError(t, c.Delete("prologic"))
45 err = c.Get("prologic", &actual)
46 require.Error(t, err)
47 assert.EqualValues(t, expected, actual)
48 })
49
50 t.Run("GetError", func(t *testing.T) {
51 db := setupDB(t)
52 defer db.Close()
53 c := db.Collection("users")
54
55 var actual User
56 err := c.Get("foo", &actual)
57 require.Error(t, err)
58 assert.ErrorIs(t, err, ErrObjectNotFound)
59 assert.EqualError(t, err, ErrObjectNotFound.Error())
60 })
61
62 t.Run("CountExistsEmpty", func(t *testing.T) {
63 db := setupDB(t)
64 defer db.Close()
65 c := db.Collection("users")
66
67 assert.Zero(t, c.Count())
68 assert.False(t, c.Exists())
69 })
70
71 t.Run("CountNonZero", func(t *testing.T) {
72 db := setupDB(t)
73 defer db.Close()
74 c := db.Collection("users")
75
76 assert.Zero(t, c.Count())
77 assert.False(t, c.Exists())
78
79 require.NoError(t, c.Add("prologic", User{"James", 21}))
80 assert.Equal(t, 1, c.Count())
81 assert.True(t, c.Exists())
82
83 })
84
85 t.Run("Has", func(t *testing.T) {
86 db := setupDB(t)
87 defer db.Close()
88 c := db.Collection("users")
89
90 assert.False(t, c.Has("prologic"))
91
92 require.NoError(t, c.Add("prologic", User{"James", 21}))
93 assert.True(t, c.Has("prologic"))
94 })
95
96 t.Run("List", func(t *testing.T) {
97 db := setupDB(t)
98 defer db.Close()
99 c := db.Collection("users")
100
101 var actual []User
102 var expected []User
103 // cannot use actual (variable of type []User) as []any value in argument to c.List
104 assert.NoError(t, c.List(actual))
105 assert.Equal(t, expected, actual)
106
107 require.NoError(t, c.Add("prologic", User{"James", 21}))
108 require.NoError(t, c.Add("bob", User{"Bob", 99})) // name is made-up
109 require.NoError(t, c.Add("frank", User{"Frank", 37})) // name is made-up
110
111 actual = []User{}
112 expected = []User{
113 {"James", 21},
114 {"Bob", 99},
115 {"frank", 37},
116 }
117
118 // cannot use actual (variable of type []User) as []any value in argument to c.List
119 assert.NoError(t, c.List(actual))
120 assert.Equal(t, expected, actual)
121 })
122}

Powered by Opengist Load: 12ms