Implement an in-memory databasewqa #11

Merged
davidlick merged 2 commits from in-memory-db into main 2021-06-19 21:56:17 +00:00
14 changed files with 935 additions and 1 deletions
+7 -1
View File
@@ -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
+3
View File
@@ -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
)
+37
View File
@@ -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=
+44
View File
@@ -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}
```
+93
View File
@@ -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
}
+318
View File
@@ -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)
})
}
}
+15
View File
@@ -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")
)
+9
View File
@@ -0,0 +1,9 @@
package ramdb
import "github.com/google/btree"
type index struct {
tree *btree.BTree
column string
table *table
}
+43
View File
@@ -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
}
+70
View File
@@ -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)
})
}
}
+51
View File
@@ -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
}
+114
View File
@@ -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)
})
}
}
+45
View File
@@ -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
}
+86
View File
@@ -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)
})
}
}