Skip to content

Commit

Permalink
Add database package
Browse files Browse the repository at this point in the history
  • Loading branch information
iamniting committed Apr 27, 2023
1 parent 3463333 commit 1a6907e
Show file tree
Hide file tree
Showing 2 changed files with 256 additions and 0 deletions.
88 changes: 88 additions & 0 deletions pkg/database/db.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package database

import "fmt"

// Database interface
type Database interface {
// Set the key to given value
Set(key string, value int)

// Get the value for the given key, set 'ok' to true if key exists
Get(key string) (value int, ok bool)

// Unset the key, making it just like that key was never set
Unset(key string)

// Begin opens a new transaction
Begin()

// Commit closes all open transaction blocks, permanently apply the
// changes made in them.
Commit() error

// Rollback undoes all of the commands issued in the most recent
// transaction block, and closes the block.
Rollback() error
}

type db struct {
data map[string]int
logs []map[string]int
}

// NewDatabase returns the Database interface
func NewDatabase() Database {
return &db{data: make(map[string]int)}
}

// Set the key to given value
func (d *db) Set(key string, value int) {
d.data[key] = value
}

// Get the value for the given key, set 'ok' to true if key exists
func (d *db) Get(key string) (int, bool) {
value, ok := d.data[key]
return value, ok
}

// Unset the key, making it just like that key was never set
func (d *db) Unset(key string) {
delete(d.data, key)
}

// Begin opens a new transaction
func (d *db) Begin() {
// Save a snapshot of the current state of the data to the logs.
snapshot := make(map[string]int)
for k, v := range d.data {
snapshot[k] = v
}
d.logs = append(d.logs, snapshot)
}

// Commit closes all open transaction blocks, permanently apply the
// changes made in them.
func (d *db) Commit() error {
// Check if there is anything to commit.
if len(d.logs) == 0 {
return fmt.Errorf("nothing to commit")
}

d.logs = nil
return nil
}

// Rollback undoes all of the commands issued in the most recent
// transaction block, and closes the block.
func (d *db) Rollback() error {
// Check if there is anything to rollback.
if len(d.logs) == 0 {
return fmt.Errorf("nothing to rollback")
}

// Restore the data to the last snapshot in the logs.
d.data = d.logs[len(d.logs)-1]
d.logs = d.logs[:len(d.logs)-1]
return nil
}
168 changes: 168 additions & 0 deletions pkg/database/db_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package database

import (
"testing"
)

func TestUnset(t *testing.T) {
db := NewDatabase()

db.Set("ex", 10)
assertValue(t, db, "ex", 10)

db.Unset("ex")
assertUnset(t, db, "ex")
}

func TestRollback(t *testing.T) {
db := NewDatabase()

db.Begin()
db.Set("a", 10)
assertValue(t, db, "a", 10)

db.Begin()
db.Set("a", 20)
assertValue(t, db, "a", 20)

err := db.Rollback()
assertNoError(t, err)
assertValue(t, db, "a", 10)

err = db.Rollback()
assertNoError(t, err)
assertUnset(t, db, "a")
}

func TestNestedCommit(t *testing.T) {
db := NewDatabase()

db.Begin()
db.Set("a", 30)

db.Begin()
db.Set("a", 40)

err := db.Commit()
assertNoError(t, err)

assertValue(t, db, "a", 40)

err = db.Rollback()
assertError(t, err)

err = db.Commit()
assertError(t, err)
}

func TestTransactionInterleavedKeys(t *testing.T) {
db := NewDatabase()

db.Set("a", 10)
db.Set("b", 10)
assertValue(t, db, "a", 10)
assertValue(t, db, "b", 10)

db.Begin()
db.Set("a", 20)
assertValue(t, db, "a", 20)
assertValue(t, db, "b", 10)

db.Begin()
db.Set("b", 30)
assertValue(t, db, "a", 20)
assertValue(t, db, "b", 30)

db.Rollback()
assertValue(t, db, "a", 20)
assertValue(t, db, "b", 10)

db.Rollback()
assertValue(t, db, "a", 10)
assertValue(t, db, "b", 10)
}

func TestTransactionRollbackUnset(t *testing.T) {
db := NewDatabase()

db.Set("a", 10)
assertValue(t, db, "a", 10)

db.Begin()
assertValue(t, db, "a", 10)
db.Set("a", 20)
assertValue(t, db, "a", 20)

db.Begin()
db.Unset("a")
assertUnset(t, db, "a")

err := db.Rollback()
assertNoError(t, err)
assertValue(t, db, "a", 20)

err = db.Commit()
assertNoError(t, err)
assertValue(t, db, "a", 20)
}

func TestTransactionCommitUnset(t *testing.T) {
db := NewDatabase()

db.Set("a", 10)
assertValue(t, db, "a", 10)

db.Begin()
assertValue(t, db, "a", 10)
db.Unset("a")
assertUnset(t, db, "a")

err := db.Rollback()
assertNoError(t, err)
assertValue(t, db, "a", 10)

db.Begin()
db.Unset("a")
assertUnset(t, db, "a")

db.Commit()
assertUnset(t, db, "a")

db.Begin()
assertUnset(t, db, "a")
db.Set("a", 20)
assertValue(t, db, "a", 20)

db.Commit()
assertValue(t, db, "a", 20)
}

func assertUnset(t *testing.T, db Database, key string) {
t.Helper()
_, ok := db.Get(key)
if ok {
t.Fatalf("key %q should not exist", key)
}
}

func assertValue(t *testing.T, db Database, key string, value int) {
t.Helper()
v, ok := db.Get(key)
if !ok || value != v {
t.Fatalf("db.Get(%q) should return %d, true: got %d, %v", key, value, v, ok)
}
}

func assertNoError(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}
}

func assertError(t *testing.T, err error) {
t.Helper()
if err == nil {
t.Fatal("expected an error, got: nil")
}
}

0 comments on commit 1a6907e

Please sign in to comment.