Last active 1699801710

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

prologic revised this gist 1699801710. Go to revision

2 files changed, 15 insertions, 10 deletions

collection.go

@@ -4,6 +4,7 @@ import (
4 4 "encoding/json"
5 5 "errors"
6 6 "fmt"
7 + "reflect"
7 8 )
8 9
9 10 var (
@@ -62,22 +63,26 @@ func (c *Collection) Get(id string, obj any) error {
62 63 }
63 64
64 65 // List returns a list of all objects in this collection.
65 - func (c *Collection) List(objects []any) error {
66 + func (c *Collection) List(objects any) error {
66 67 tx := c.db.Transaction()
67 68 defer tx.Discard()
68 69
70 + value := reflect.ValueOf(objects).Elem()
71 + elem := reflect.TypeOf(objects).Elem()
72 +
69 73 return tx.Scan(c.makePrefix(), func(k Key) error {
70 74 data, err := tx.Get(k)
71 75 if err != nil {
72 76 return err
73 77 }
74 78
75 - var obj any
76 - if err := json.Unmarshal(data, obj); err != nil {
79 + obj := reflect.New(elem)
80 +
81 + if err := json.Unmarshal(data, &obj); err != nil {
77 82 return err
78 83 }
79 84
80 - objects = append(objects, obj)
85 + value.Set(reflect.Append(value, reflect.ValueOf(obj)))
81 86
82 87 return nil
83 88 })
@@ -117,4 +122,4 @@ func (c *Collection) Drop() error {
117 122 // Count() > 0.
118 123 func (c *Collection) Exists() bool {
119 124 return c.Count() > 0
120 - }
125 + }

collection_test.go

@@ -99,7 +99,7 @@ func TestCollection(t *testing.T) {
99 99 c := db.Collection("users")
100 100
101 101 var actual []User
102 - expected := []User{}
102 + var expected []User
103 103 // cannot use actual (variable of type []User) as []any value in argument to c.List
104 104 assert.NoError(t, c.List(actual))
105 105 assert.Equal(t, expected, actual)
@@ -110,13 +110,13 @@ func TestCollection(t *testing.T) {
110 110
111 111 actual = []User{}
112 112 expected = []User{
113 - User{"James", 21},
114 - User{"Bob", 99},
115 - User{"frank", 37},
113 + {"James", 21},
114 + {"Bob", 99},
115 + {"frank", 37},
116 116 }
117 117
118 118 // cannot use actual (variable of type []User) as []any value in argument to c.List
119 119 assert.NoError(t, c.List(actual))
120 120 assert.Equal(t, expected, actual)
121 121 })
122 - }
122 + }

prologic revised this gist 1699800548. Go to revision

2 files changed, 242 insertions

collection.go(file created)

@@ -0,0 +1,120 @@
1 + package bitcask
2 +
3 + import (
4 + "encoding/json"
5 + "errors"
6 + "fmt"
7 + )
8 +
9 + var (
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 +
14 + func (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.
21 + type Collection struct {
22 + db DB
23 + name string
24 + }
25 +
26 + func (c *Collection) makeKey(id string) Key {
27 + return Key(fmt.Sprintf("%s/%s", c.name, id))
28 + }
29 +
30 + func (c *Collection) makePrefix() Key {
31 + return Key(c.name)
32 + }
33 +
34 + // Add adds a new object to the collection
35 + func (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
46 + func (c *Collection) Delete(id string) error {
47 + return c.db.Delete(c.makeKey(id))
48 + }
49 +
50 + // Get returns an object from the collection
51 + func (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.
65 + func (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.
87 + func (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.
92 + func (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
102 + func (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.
118 + func (c *Collection) Exists() bool {
119 + return c.Count() > 0
120 + }

collection_test.go(file created)

@@ -0,0 +1,122 @@
1 + package bitcask
2 +
3 + import (
4 + "os"
5 + "testing"
6 +
7 + "github.com/stretchr/testify/assert"
8 + "github.com/stretchr/testify/require"
9 + )
10 +
11 + func 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 +
23 + func 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 + }
Newer Older

Powered by Opengist Load: 23ms