Implement an in-memory databasewqa
This commit is contained in:
@@ -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,82 @@
|
||||
package ramdb
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTable_NewIndex(t *testing.T) {
|
||||
tests := []struct {
|
||||
test string
|
||||
table table
|
||||
onColumn string
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
test: "it should return ErrInvalidIndex if onColumn is empty",
|
||||
expectedError: ErrInvalidIndex,
|
||||
},
|
||||
{
|
||||
test: "it should return ErrIndexExists if an index exists for onColumn",
|
||||
table: table{indexes: map[string]*index{
|
||||
"test_column": &index{},
|
||||
}},
|
||||
onColumn: "test_column",
|
||||
expectedError: ErrIndexExists,
|
||||
},
|
||||
{
|
||||
test: "it should create an index successfully",
|
||||
table: table{
|
||||
indexes: make(map[string]*index),
|
||||
},
|
||||
onColumn: "test_column",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.test, func(t *testing.T) {
|
||||
err := tc.table.CreateIndex(tc.onColumn)
|
||||
|
||||
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