Last active 1699801710

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

Revision 6c768edd72b6f82f4fed8746e3f14488fce986f7

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

Powered by Opengist Load: 11ms