collection.go
Raw
package bitcask
import (
"encoding/json"
"errors"
"fmt"
)
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()
return tx.Scan(c.makePrefix(), func(k Key) error {
data, err := tx.Get(k)
if err != nil {
return err
}
var obj any
if err := json.Unmarshal(data, obj); err != nil {
return err
}
objects = append(objects, 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 | ) |
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 | } |
121 |
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
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{
User{"James", 21},
User{"Bob", 99},
User{"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 | 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 |