Implement an in-memory databasewqa #11
@@ -10,6 +10,12 @@ integration-test:
|
||||
all-test:
|
||||
go test ./...
|
||||
|
||||
test-coverage:
|
||||
go test -cover ./...
|
||||
|
||||
cover-report:
|
||||
go test -coverprofile=coverage.out ./... && go tool cover -html=coverage.out
|
||||
|
||||
build:
|
||||
go build -o supermarket-api cmd/api/*.go
|
||||
|
||||
@@ -17,4 +23,4 @@ run:
|
||||
go run cmd/api/*.go
|
||||
|
||||
clean:
|
||||
rm ./supermarket-api
|
||||
rm ./supermarket-api ./coverage.out
|
||||
|
||||
@@ -4,7 +4,10 @@ go 1.16
|
||||
|
||||
require (
|
||||
github.com/go-chi/chi v1.5.4 // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/google/btree v1.0.1 // indirect
|
||||
github.com/joho/godotenv v1.3.0 // indirect
|
||||
github.com/kelseyhightower/envconfig v1.4.0 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
github.com/stretchr/testify v1.7.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,13 +1,50 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs=
|
||||
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
|
||||
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
|
||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
|
||||
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# ramdb
|
||||
|
||||
RamDB is an implementation of an in-memory database with a simple API for selecting and querying the database. It uses b-trees as the underlying storage mechanism which allows fast searches and mutations.
|
||||
|
||||
All database commands are safe for concurrent operations.
|
||||
|
||||
## Example
|
||||
|
||||
```go
|
||||
// Create database and table.
|
||||
db := ramdb.NewDatabase()
|
||||
_ = db.CreateTable("hotdogs", "frank_id")
|
||||
|
||||
// Mm, hotdogs.
|
||||
type HotDog struct {
|
||||
FrankId string
|
||||
Condiments []string
|
||||
Brat bool
|
||||
}
|
||||
|
||||
indog := HotDog{
|
||||
FrankId: "1",
|
||||
Condiments: []string{
|
||||
"kraut",
|
||||
"mustard",
|
||||
},
|
||||
Brat: true,
|
||||
}
|
||||
|
||||
// Create a new record and insert.
|
||||
rec, _ := ramdb.NewRecord("1", "frank_id", indog)
|
||||
_ = db.From("hotdogs").Insert(rec)
|
||||
|
||||
|
||||
// Query data out.
|
||||
ro, _ := db.From("hotdogs").Get("frank_id", "1")
|
||||
|
||||
var outdog HotDog
|
||||
_ = ro.Deserialize(&outdog)
|
||||
|
||||
fmt.Printf("%+v\n", outdog)
|
||||
|
||||
// &HotDog{"1" ["kraut", "mustard"] true}
|
||||
```
|
||||
@@ -0,0 +1,93 @@
|
||||
package ramdb
|
||||
|
||||
import "github.com/google/btree"
|
||||
|
||||
// Get validates that the table and index for the given column exists and searches for the given key in the tree.
|
||||
func (t *table) Get(column, key string) (r *record, err error) {
|
||||
if !t.exists {
|
||||
return nil, ErrNoTable
|
||||
}
|
||||
|
||||
if !t.HasIndex(column) {
|
||||
return nil, ErrNoIndex
|
||||
}
|
||||
|
||||
return t.keyLookup(key, t.indexes[column])
|
||||
}
|
||||
|
||||
// keyLookup tries to find the given key in the tree. It returns ErrNoRecord if not found.
|
||||
func (t *table) keyLookup(key string, index *index) (r *record, err error) {
|
||||
item := &record{id: keyHash(key)}
|
||||
result := index.tree.Get(item)
|
||||
if result == nil {
|
||||
return nil, ErrNoRecord
|
||||
}
|
||||
|
||||
return result.(*record), nil
|
||||
}
|
||||
|
||||
// Select returns all of the records in the database sorted in ascending order by id.
|
||||
func (t *table) Select(column string) (rr []*record, err error) {
|
||||
if !t.exists {
|
||||
return nil, ErrNoTable
|
||||
}
|
||||
|
||||
if !t.HasIndex(column) {
|
||||
return nil, ErrNoIndex
|
||||
}
|
||||
|
||||
t.indexes[column].tree.Ascend(func(item btree.Item) bool {
|
||||
r := item.(*record)
|
||||
rr = append(rr, r)
|
||||
return true
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Insert adds the record to the database. It returns ErrRecordExists if the record already exists. Insert is thread safe.
|
||||
func (t *table) Insert(r *record) error {
|
||||
if !t.exists {
|
||||
return ErrNoTable
|
||||
}
|
||||
|
||||
if !t.HasIndex(r.keyColumn) {
|
||||
return ErrNoIndex
|
||||
}
|
||||
|
||||
index := t.indexes[r.keyColumn]
|
||||
|
||||
if has := index.tree.Has(r); has {
|
||||
return ErrRecordExists
|
||||
}
|
||||
|
||||
t.mutex.Lock()
|
||||
defer t.mutex.Unlock()
|
||||
|
||||
index.tree.ReplaceOrInsert(r)
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// Delete removes the item from the database. It returns ErrNoRecord if the record does not exist and ErrNotDeleted if the removal fails. Delete is thread safe.
|
||||
func (t *table) Delete(r *record) error {
|
||||
if !t.exists {
|
||||
return ErrNoTable
|
||||
}
|
||||
|
||||
if !t.HasIndex(r.keyColumn) {
|
||||
return ErrNoIndex
|
||||
}
|
||||
|
||||
index := t.indexes[r.keyColumn]
|
||||
|
||||
if has := index.tree.Has(r); !has {
|
||||
return ErrNoRecord
|
||||
}
|
||||
|
||||
t.mutex.Lock()
|
||||
defer t.mutex.Unlock()
|
||||
|
||||
index.tree.Delete(r)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
package ramdb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/google/btree"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTable_Get(t *testing.T) {
|
||||
tests := []struct {
|
||||
test string
|
||||
tableConfig func() *table
|
||||
expectedRecord *record
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
test: "it should return ErrNoTable if an invalid table is supplied",
|
||||
tableConfig: func() *table {
|
||||
return &table{}
|
||||
},
|
||||
expectedError: ErrNoTable,
|
||||
},
|
||||
{
|
||||
test: "it should return ErrNoIndex if no index exists for column",
|
||||
tableConfig: func() *table {
|
||||
return &table{
|
||||
exists: true,
|
||||
}
|
||||
},
|
||||
expectedError: ErrNoIndex,
|
||||
},
|
||||
{
|
||||
test: "it should return ErrNoRecord if no record was found for key",
|
||||
tableConfig: func() *table {
|
||||
return &table{
|
||||
exists: true,
|
||||
indexes: map[string]*index{
|
||||
"test_column": &index{
|
||||
tree: btree.New(5),
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
expectedError: ErrNoRecord,
|
||||
},
|
||||
{
|
||||
test: "it should return a record when one is found",
|
||||
tableConfig: func() *table {
|
||||
tbl := &table{
|
||||
exists: true,
|
||||
indexes: map[string]*index{
|
||||
"test_column": &index{
|
||||
tree: btree.New(5),
|
||||
},
|
||||
},
|
||||
}
|
||||
rec, err := NewRecord("test_key", "test_column", struct{}{})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
tbl.indexes["test_column"].tree.ReplaceOrInsert(rec)
|
||||
return tbl
|
||||
},
|
||||
expectedRecord: &record{
|
||||
serialized: []uint8{0x7b, 0x7d},
|
||||
key: "test_key",
|
||||
keyColumn: "test_column",
|
||||
id: 0x92488e1e3eeecdf9,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.test, func(t *testing.T) {
|
||||
tbl := tc.tableConfig()
|
||||
|
||||
record, err := tbl.Get("test_column", "test_key")
|
||||
|
||||
assert.Equal(t, tc.expectedRecord, record)
|
||||
assert.Equal(t, tc.expectedError, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTable_Select(t *testing.T) {
|
||||
tests := []struct {
|
||||
test string
|
||||
tableConfig func() (*table, []*record)
|
||||
expectedRecords []*record
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
test: "it should return ErrNoTable if an invalid table is supplied",
|
||||
tableConfig: func() (*table, []*record) {
|
||||
return &table{
|
||||
mutex: &sync.Mutex{},
|
||||
}, nil
|
||||
},
|
||||
expectedError: ErrNoTable,
|
||||
},
|
||||
{
|
||||
test: "it should return ErrNoIndex if no index exists for column",
|
||||
tableConfig: func() (*table, []*record) {
|
||||
return &table{
|
||||
exists: true,
|
||||
mutex: &sync.Mutex{},
|
||||
indexes: make(map[string]*index),
|
||||
}, nil
|
||||
},
|
||||
expectedError: ErrNoIndex,
|
||||
},
|
||||
{
|
||||
test: "it should return all records in the database",
|
||||
tableConfig: func() (*table, []*record) {
|
||||
tbl := &table{
|
||||
exists: true,
|
||||
mutex: &sync.Mutex{},
|
||||
indexes: map[string]*index{
|
||||
"test_column": &index{
|
||||
tree: btree.New(5),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expectedRecords := make([]*record, 0)
|
||||
for i := 0; i < 10; i++ {
|
||||
key := fmt.Sprintf("key-%d", i)
|
||||
rec, err := NewRecord(key, "test_column", struct{}{})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
tbl.indexes["test_column"].tree.ReplaceOrInsert(rec)
|
||||
expectedRecords = append(expectedRecords, rec)
|
||||
}
|
||||
|
||||
sort.Slice(expectedRecords, func(a, b int) bool {
|
||||
return expectedRecords[a].id < expectedRecords[b].id
|
||||
})
|
||||
|
||||
return tbl, expectedRecords
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.test, func(t *testing.T) {
|
||||
tbl, expectedRecords := tc.tableConfig()
|
||||
|
||||
rr, err := tbl.Select("test_column")
|
||||
|
||||
assert.Equal(t, expectedRecords, rr)
|
||||
assert.Equal(t, tc.expectedError, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTable_Insert(t *testing.T) {
|
||||
tests := []struct {
|
||||
test string
|
||||
tableConfig func() *table
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
test: "it should return ErrNoTable if an invalid table is supplied",
|
||||
tableConfig: func() *table {
|
||||
return &table{
|
||||
mutex: &sync.Mutex{},
|
||||
}
|
||||
},
|
||||
expectedError: ErrNoTable,
|
||||
},
|
||||
{
|
||||
test: "it should return ErrNoIndex if no index exists for column",
|
||||
tableConfig: func() *table {
|
||||
return &table{
|
||||
exists: true,
|
||||
mutex: &sync.Mutex{},
|
||||
indexes: make(map[string]*index),
|
||||
}
|
||||
},
|
||||
expectedError: ErrNoIndex,
|
||||
},
|
||||
{
|
||||
test: "it should return ErrRecordExists if a record with key exists",
|
||||
tableConfig: func() *table {
|
||||
tbl := &table{
|
||||
exists: true,
|
||||
mutex: &sync.Mutex{},
|
||||
indexes: map[string]*index{
|
||||
"test_column": &index{
|
||||
tree: btree.New(5),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rec, err := NewRecord("test_key", "test_column", struct{}{})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
tbl.indexes["test_column"].tree.ReplaceOrInsert(rec)
|
||||
|
||||
return tbl
|
||||
},
|
||||
expectedError: ErrRecordExists,
|
||||
},
|
||||
{
|
||||
test: "it should return no error if successful",
|
||||
tableConfig: func() *table {
|
||||
return &table{
|
||||
exists: true,
|
||||
mutex: &sync.Mutex{},
|
||||
indexes: map[string]*index{
|
||||
"test_column": &index{
|
||||
tree: btree.New(5),
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.test, func(t *testing.T) {
|
||||
tbl := tc.tableConfig()
|
||||
rec, err := NewRecord("test_key", "test_column", struct{}{})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = tbl.Insert(rec)
|
||||
|
||||
assert.Equal(t, tc.expectedError, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTable_Delete(t *testing.T) {
|
||||
tests := []struct {
|
||||
test string
|
||||
tableConfig func() *table
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
test: "it should return ErrNoTable if an invalid table is supplied",
|
||||
tableConfig: func() *table {
|
||||
return &table{
|
||||
mutex: &sync.Mutex{},
|
||||
}
|
||||
},
|
||||
expectedError: ErrNoTable,
|
||||
},
|
||||
{
|
||||
test: "it should return ErrNoIndex if no index exists for column",
|
||||
tableConfig: func() *table {
|
||||
return &table{
|
||||
exists: true,
|
||||
mutex: &sync.Mutex{},
|
||||
indexes: make(map[string]*index),
|
||||
}
|
||||
},
|
||||
expectedError: ErrNoIndex,
|
||||
},
|
||||
{
|
||||
test: "it should return ErrNoRecord if the record does not exist",
|
||||
tableConfig: func() *table {
|
||||
return &table{
|
||||
exists: true,
|
||||
mutex: &sync.Mutex{},
|
||||
indexes: map[string]*index{
|
||||
"test_column": &index{
|
||||
tree: btree.New(5),
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
expectedError: ErrNoRecord,
|
||||
},
|
||||
{
|
||||
test: "it should return no error if the item was deleted",
|
||||
tableConfig: func() *table {
|
||||
tbl := &table{
|
||||
exists: true,
|
||||
mutex: &sync.Mutex{},
|
||||
indexes: map[string]*index{
|
||||
"test_column": &index{
|
||||
tree: btree.New(5),
|
||||
},
|
||||
},
|
||||
}
|
||||
rec, err := NewRecord("test_key", "test_column", struct{}{})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
tbl.indexes["test_column"].tree.ReplaceOrInsert(rec)
|
||||
return tbl
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.test, func(t *testing.T) {
|
||||
tbl := tc.tableConfig()
|
||||
rec, err := NewRecord("test_key", "test_column", struct{}{})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = tbl.Delete(rec)
|
||||
|
||||
assert.Equal(t, tc.expectedError, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package ramdb
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrNoTable = errors.New("table does not exist")
|
||||
ErrTableExists = errors.New("table already exists")
|
||||
|
||||
ErrNoRecord = errors.New("record does not exist")
|
||||
ErrRecordExists = errors.New("record already exists")
|
||||
|
||||
ErrNoIndex = errors.New("index does not exist")
|
||||
ErrInvalidIndex = errors.New("invalid index column")
|
||||
ErrIndexExists = errors.New("index already exists")
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
package ramdb
|
||||
|
||||
import "github.com/google/btree"
|
||||
|
||||
type index struct {
|
||||
tree *btree.BTree
|
||||
column string
|
||||
table *table
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package ramdb
|
||||
|
||||
type database struct {
|
||||
tables map[string]*table
|
||||
}
|
||||
|
||||
// NewDatabase initializes a new database with no tables.
|
||||
func NewDatabase() *database {
|
||||
return &database{
|
||||
tables: make(map[string]*table),
|
||||
}
|
||||
}
|
||||
|
||||
// From selects a table for running commands.
|
||||
func (db *database) From(tablename string) *table {
|
||||
t, ok := db.tables[tablename]
|
||||
if !ok {
|
||||
return &table{}
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
// CreateTable creates a new table in the database with indexes for each column specified.
|
||||
func (db *database) CreateTable(tablename string, indexOnColumns ...string) error {
|
||||
if _, found := db.tables[tablename]; found {
|
||||
return ErrTableExists
|
||||
}
|
||||
|
||||
tbl := &table{
|
||||
exists: true,
|
||||
}
|
||||
|
||||
for _, onColumn := range indexOnColumns {
|
||||
err := tbl.CreateIndex(onColumn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
db.tables[tablename] = tbl
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package ramdb
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDatabase_From(t *testing.T) {
|
||||
tests := []struct {
|
||||
test string
|
||||
expectFunc func(t *testing.T, db *database)
|
||||
expectedExists bool
|
||||
}{
|
||||
{
|
||||
test: "it should set exist false if table not found",
|
||||
expectFunc: func(t *testing.T, db *database) {},
|
||||
expectedExists: false,
|
||||
},
|
||||
{
|
||||
test: "it should set exist true if table is found",
|
||||
expectFunc: func(t *testing.T, db *database) {
|
||||
db.CreateTable("test_table")
|
||||
},
|
||||
expectedExists: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.test, func(t *testing.T) {
|
||||
db := NewDatabase()
|
||||
tc.expectFunc(t, db)
|
||||
|
||||
table := db.From("test_table")
|
||||
|
||||
assert.Equal(t, table.exists, tc.expectedExists)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabase_CreateTable(t *testing.T) {
|
||||
tests := []struct {
|
||||
test string
|
||||
expectFunc func(t *testing.T, db *database)
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
test: "it should error if the table already exists",
|
||||
expectFunc: func(t *testing.T, db *database) {
|
||||
db.CreateTable("test_table")
|
||||
},
|
||||
expectedError: ErrTableExists,
|
||||
},
|
||||
{
|
||||
test: "it should not return an error on successful table creation",
|
||||
expectFunc: func(t *testing.T, db *database) {},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.test, func(t *testing.T) {
|
||||
db := NewDatabase()
|
||||
tc.expectFunc(t, db)
|
||||
|
||||
err := db.CreateTable("test_table")
|
||||
|
||||
assert.Equal(t, tc.expectedError, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package ramdb
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/google/btree"
|
||||
)
|
||||
|
||||
type record struct {
|
||||
serialized []byte
|
||||
keyColumn string
|
||||
key string
|
||||
id uint64
|
||||
}
|
||||
|
||||
// NewRecord returns a pointer to a record populated with data, key, and a hash of the key used for ordering in the tree.
|
||||
func NewRecord(key, keyColumn string, data interface{}) (*record, error) {
|
||||
serialized, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var r record
|
||||
r.serialized = serialized
|
||||
r.keyColumn = keyColumn
|
||||
r.key = key
|
||||
r.id = keyHash(key)
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
func keyHash(s string) uint64 {
|
||||
h := sha256.New()
|
||||
h.Write([]byte(s))
|
||||
sum := h.Sum(nil)
|
||||
|
||||
return binary.BigEndian.Uint64(sum)
|
||||
}
|
||||
|
||||
// Deserialize unmarshals the serialized data into `into`.
|
||||
func (r *record) Deserialize(into interface{}) error {
|
||||
err := json.Unmarshal(r.serialized, &into)
|
||||
return err
|
||||
}
|
||||
|
||||
// Less is used to order items and for looking up records in the tree.
|
||||
func (r *record) Less(than btree.Item) bool {
|
||||
re := than.(*record)
|
||||
return r.id < re.id
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package ramdb
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRecord_NewRecord(t *testing.T) {
|
||||
tests := []struct {
|
||||
test string
|
||||
key string
|
||||
keyColumn string
|
||||
data interface{}
|
||||
expectedRecord *record
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
test: "it should error if json serialization fails",
|
||||
key: "test-record",
|
||||
keyColumn: "test-column",
|
||||
data: map[string]interface{}{
|
||||
"error": make(chan int),
|
||||
},
|
||||
expectedError: "json: unsupported type: chan int",
|
||||
},
|
||||
{
|
||||
test: "it should return a valid record when successful",
|
||||
key: "test-record",
|
||||
keyColumn: "test-column",
|
||||
data: struct{}{},
|
||||
expectedRecord: &record{
|
||||
serialized: []byte("{}"),
|
||||
key: "test-record",
|
||||
keyColumn: "test-column",
|
||||
id: 0x267fc212f178ef79,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.test, func(t *testing.T) {
|
||||
r, err := NewRecord(tc.key, tc.keyColumn, tc.data)
|
||||
|
||||
assert.Equal(t, tc.expectedRecord, r)
|
||||
if tc.expectedError != "" {
|
||||
assert.Equal(t, tc.expectedError, err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecord_keyHash(t *testing.T) {
|
||||
tests := []struct {
|
||||
test string
|
||||
input string
|
||||
output uint64
|
||||
}{
|
||||
{
|
||||
test: "it should hash the input deterministically",
|
||||
input: "test string",
|
||||
output: 0xd5579c46dfcc7f18,
|
||||
},
|
||||
{
|
||||
test: "it should produce an entirely different value with a small change to input",
|
||||
input: "test string.",
|
||||
output: 0x84083c0b244440c0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.test, func(t *testing.T) {
|
||||
result := keyHash(tc.input)
|
||||
|
||||
assert.Equal(t, tc.output, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecord_Deserialize(t *testing.T) {
|
||||
tests := []struct {
|
||||
test string
|
||||
into struct{ Key string }
|
||||
expectedInto struct{ Key string }
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
test: "it should unmarshal record data into into",
|
||||
into: struct {
|
||||
Key string
|
||||
}{},
|
||||
expectedInto: struct {
|
||||
Key string
|
||||
}{
|
||||
Key: "test string",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.test, func(t *testing.T) {
|
||||
rec, err := NewRecord("", "", struct{ Key string }{Key: "test string"})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
into := tc.into
|
||||
err = rec.Deserialize(&into)
|
||||
|
||||
assert.Equal(t, tc.expectedInto, into)
|
||||
assert.Equal(t, tc.expectedError, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package ramdb
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/google/btree"
|
||||
)
|
||||
|
||||
type table struct {
|
||||
exists bool
|
||||
mutex *sync.Mutex
|
||||
indexes map[string]*index
|
||||
}
|
||||
|
||||
// CreateIndex creates an index for onColumn.
|
||||
func (t *table) CreateIndex(column string) error {
|
||||
if column == "" {
|
||||
return ErrInvalidIndex
|
||||
}
|
||||
|
||||
if _, found := t.indexes[column]; found {
|
||||
return ErrIndexExists
|
||||
}
|
||||
|
||||
idx := &index{
|
||||
tree: btree.New(5),
|
||||
column: column,
|
||||
table: t,
|
||||
}
|
||||
|
||||
t.mutex.Lock()
|
||||
defer t.mutex.Unlock()
|
||||
|
||||
t.indexes[column] = idx
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasIndex returns true if an index exists for the column and false if it does not.
|
||||
func (t *table) HasIndex(column string) bool {
|
||||
if _, found := t.indexes[column]; found {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package ramdb
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTable_CreateIndex(t *testing.T) {
|
||||
tests := []struct {
|
||||
test string
|
||||
table table
|
||||
column string
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
test: "it should return ErrInvalidIndex if column is empty",
|
||||
expectedError: ErrInvalidIndex,
|
||||
},
|
||||
{
|
||||
test: "it should return ErrIndexExists if an index exists for column",
|
||||
table: table{
|
||||
mutex: &sync.Mutex{},
|
||||
indexes: map[string]*index{
|
||||
"test_column": &index{},
|
||||
}},
|
||||
column: "test_column",
|
||||
expectedError: ErrIndexExists,
|
||||
},
|
||||
{
|
||||
test: "it should create an index successfully",
|
||||
table: table{
|
||||
mutex: &sync.Mutex{},
|
||||
indexes: make(map[string]*index),
|
||||
},
|
||||
column: "test_column",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.test, func(t *testing.T) {
|
||||
err := tc.table.CreateIndex(tc.column)
|
||||
|
||||
assert.Equal(t, tc.expectedError, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTable_HasIndex(t *testing.T) {
|
||||
tests := []struct {
|
||||
test string
|
||||
tableConfig func() *table
|
||||
expectedHas bool
|
||||
}{
|
||||
{
|
||||
test: "it should return true if index exists",
|
||||
tableConfig: func() *table {
|
||||
return &table{
|
||||
indexes: map[string]*index{
|
||||
"test_column": &index{},
|
||||
},
|
||||
}
|
||||
},
|
||||
expectedHas: true,
|
||||
},
|
||||
{
|
||||
test: "it should return false if index does not exist",
|
||||
tableConfig: func() *table {
|
||||
return &table{
|
||||
indexes: make(map[string]*index),
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.test, func(t *testing.T) {
|
||||
tbl := tc.tableConfig()
|
||||
|
||||
has := tbl.HasIndex("test_column")
|
||||
|
||||
assert.Equal(t, tc.expectedHas, has)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user