collection.go
Raw
package bitcask
import (
"encoding/json"
"errors"
"fmt"
"reflect"
)
var (
// ErrObjectNotFound is the error returned when an object is not found in the collection.
ErrObjectNotFound = errors.New("error: object not found")
)
func (b *bitcask) Collection(name string) *Collection {
return &Collection{db: b, name: name}
}
// Collection allows you to manage a collection of objects encoded as JSON
// documents with a path-based key based on the provided name. This is convenient
// for storing complex normalized collections of objects.
type Collection struct {
db DB
name string
}
func (c *Collection) makeKey(id string) Key {
return Key(fmt.Sprintf("%s/%s", c.name, id))
}
func (c *Collection) makePrefix() Key {
return Key(c.name)
}
// Add adds a new object to the collection
func (c *Collection) Add(id string, obj any) error {
k := c.makeKey(id)
v, err := json.Marshal(obj)
if err != nil {
return err
}
return c.db.Put(k, v)
}
// Delete deletes an object from the collection
func (c *Collection) Delete(id string) error {
return c.db.Delete(c.makeKey(id))
}
// Get returns an object from the collection
func (c *Collection) Get(id string, obj any) error {
k := c.makeKey(id)
data, err := c.db.Get(k)
if err != nil {
if err == ErrKeyNotFound {
return ErrObjectNotFound
}
return err
}
return json.Unmarshal(data, obj)
}
// List returns a list of all objects in this collection.
func (c *Collection) List(objects any) error {
tx := c.db.Transaction()
defer tx.Discard()
value := reflect.ValueOf(objects).Elem()
elem := reflect.TypeOf(objects).Elem()
return tx.Scan(c.makePrefix(), func(k Key) error {
data, err := tx.Get(k)
if err != nil {
return err
}
obj := reflect.New(elem)
if err := json.Unmarshal(data, &obj); err != nil {
return err
}
value.Set(reflect.Append(value, reflect.ValueOf(obj)))
return nil
})
}
// Has returns true if an object exists by the provided id.
func (c *Collection) Has(id string) bool {
return c.db.Has(c.makeKey(id))
}
// Count returns the number of objects in this collection.
func (c *Collection) Count() int {
n := 0
c.db.Scan(c.makePrefix(), func(k Key) error {
n++
return nil
})
return n
}
// Drop deletes the entire collection
func (c *Collection) Drop() error {
tx := c.db.Transaction()
defer tx.Discard()
if err := tx.Scan(c.makePrefix(), func(k Key) error {
return tx.Delete(k)
}); err != nil {
return err
}
return tx.Commit()
}
// Exists returns true if the collection exists at all, which really just means
// whether there are any objects in the collection, so this is the same as calling
// Count() > 0.
func (c *Collection) Exists() bool {
return c.Count() > 0
}
1 | package bitcask |
2 | |
3 | import ( |
4 | "encoding/json" |
5 | "errors" |
6 | "fmt" |
7 | "reflect" |
8 | ) |
9 | |
10 | var ( |
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 | |
15 | func (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. |
22 | type Collection struct { |
23 | db DB |
24 | name string |
25 | } |
26 | |
27 | func (c *Collection) makeKey(id string) Key { |
28 | return Key(fmt.Sprintf("%s/%s", c.name, id)) |
29 | } |
30 | |
31 | func (c *Collection) makePrefix() Key { |
32 | return Key(c.name) |
33 | } |
34 | |
35 | // Add adds a new object to the collection |
36 | func (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 |
47 | func (c *Collection) Delete(id string) error { |
48 | return c.db.Delete(c.makeKey(id)) |
49 | } |
50 | |
51 | // Get returns an object from the collection |
52 | func (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. |
66 | func (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. |
92 | func (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. |
97 | func (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 |
107 | func (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. |
123 | func (c *Collection) Exists() bool { |
124 | return c.Count() > 0 |
125 | } |
collection_test.go
Raw
package bitcask
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func setupDB(t *testing.T) DB {
t.Helper()
testDir, err := os.MkdirTemp("", "bitcask")
assert.NoError(t, err)
db, err := Open(testDir)
assert.NoError(t, err)
return db
}
func TestCollection(t *testing.T) {
type User struct {
Name string
Age int
}
t.Run("AddGetDelete", func(t *testing.T) {
db := setupDB(t)
defer db.Close()
c := db.Collection("users")
err := c.Add("prologic", &User{"James", 21})
assert.NoError(t, err)
var actual User
expected := User{"James", 21}
require.NoError(t, c.Get("prologic", &actual))
assert.EqualValues(t, expected, actual)
actual = User{}
expected = User{}
require.NoError(t, c.Delete("prologic"))
err = c.Get("prologic", &actual)
require.Error(t, err)
assert.EqualValues(t, expected, actual)
})
t.Run("GetError", func(t *testing.T) {
db := setupDB(t)
defer db.Close()
c := db.Collection("users")
var actual User
err := c.Get("foo", &actual)
require.Error(t, err)
assert.ErrorIs(t, err, ErrObjectNotFound)
assert.EqualError(t, err, ErrObjectNotFound.Error())
})
t.Run("CountExistsEmpty", func(t *testing.T) {
db := setupDB(t)
defer db.Close()
c := db.Collection("users")
assert.Zero(t, c.Count())
assert.False(t, c.Exists())
})
t.Run("CountNonZero", func(t *testing.T) {
db := setupDB(t)
defer db.Close()
c := db.Collection("users")
assert.Zero(t, c.Count())
assert.False(t, c.Exists())
require.NoError(t, c.Add("prologic", User{"James", 21}))
assert.Equal(t, 1, c.Count())
assert.True(t, c.Exists())
})
t.Run("Has", func(t *testing.T) {
db := setupDB(t)
defer db.Close()
c := db.Collection("users")
assert.False(t, c.Has("prologic"))
require.NoError(t, c.Add("prologic", User{"James", 21}))
assert.True(t, c.Has("prologic"))
})
t.Run("List", func(t *testing.T) {
db := setupDB(t)
defer db.Close()
c := db.Collection("users")
var actual []User
var expected []User
// cannot use actual (variable of type []User) as []any value in argument to c.List
assert.NoError(t, c.List(actual))
assert.Equal(t, expected, actual)
require.NoError(t, c.Add("prologic", User{"James", 21}))
require.NoError(t, c.Add("bob", User{"Bob", 99})) // name is made-up
require.NoError(t, c.Add("frank", User{"Frank", 37})) // name is made-up
actual = []User{}
expected = []User{
{"James", 21},
{"Bob", 99},
{"frank", 37},
}
// cannot use actual (variable of type []User) as []any value in argument to c.List
assert.NoError(t, c.List(actual))
assert.Equal(t, expected, actual)
})
}
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 | 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 | } |