From d4c9b20b2b7c068cbe15c71ab4fd3eac89dd1971 Mon Sep 17 00:00:00 2001 From: Fabio Ribeiro Date: Wed, 14 Mar 2018 00:38:35 -0300 Subject: [PATCH] SQLite3 support * SQLite3 caching support --- Gopkg.lock | 8 +- Gopkg.toml | 4 + Makefile | 2 +- README.md | 19 +++++ sqlite3.go | 186 +++++++++++++++++++++++++++++++++++++++++++++++ sqlite3_test.go | 190 ++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 407 insertions(+), 2 deletions(-) create mode 100644 sqlite3.go create mode 100644 sqlite3_test.go diff --git a/Gopkg.lock b/Gopkg.lock index ebefde0..861f1ab 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -12,6 +12,12 @@ revision = "346938d642f2ec3594ed81d874461961cd0faa76" version = "v1.1.0" +[[projects]] + name = "github.com/mattn/go-sqlite3" + packages = ["."] + revision = "6c771bb9887719704b210e87e934f08be014bdb1" + version = "v1.6.0" + [[projects]] name = "github.com/pkg/errors" packages = ["."] @@ -69,6 +75,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "b7085a9dbdc6ab368ae967789be61f9eb149276215063b199a97d927a86a08ea" + inputs-digest = "181181306e458ac82fbe89f3be2c65a37f9260f3b7e40c1dcaae95371df34134" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 24180fa..f92a246 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -45,6 +45,10 @@ name = "gopkg.in/redis.v4" version = "4.2.4" +[[constraint]] + name = "github.com/mattn/go-sqlite3" + version = "1.6.0" + [prune] go-tests = true unused-packages = true diff --git a/Makefile b/Makefile index 3d61e9f..1507b38 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,6 @@ configure: test: @go test -v . -test-coverage: configure +test-coverage: @go test -coverprofile=cover.out -v . @go tool cover -html=cover.out -o cover.html diff --git a/README.md b/README.md index d46b331..4822544 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,25 @@ func init() { } ``` +### Sqlite3 + +```go +package main + +import ( + "database/sql" + _ "github.com/mattn/go-sqlite3" +) + +var cache cachego.Cache + +func init() { + db, _ := sql.Open("sqlite3", "./cache.db") + + cache, _ = NewSqlite3(db, "cache") +} +``` + ### Chain ```go diff --git a/sqlite3.go b/sqlite3.go new file mode 100644 index 0000000..1cd2264 --- /dev/null +++ b/sqlite3.go @@ -0,0 +1,186 @@ +package cachego + +import ( + "database/sql" + "fmt" + errors "github.com/pkg/errors" + "time" +) + +type ( + // Sqlite3 store for caching data + Sqlite3 struct { + db *sql.DB + table string + } +) + +// NewSqlite3 - Create an instance of Sqlite3 +func NewSqlite3(db *sql.DB, table string) (*Sqlite3, error) { + if err := createTable(db, table); err != nil { + return nil, errors.Wrap(err, "Unable to create database table") + } + + return &Sqlite3{db, table}, nil +} + +func createTable(db *sql.DB, table string) error { + stmt := `CREATE TABLE IF NOT EXISTS %s ( + key text PRIMARY KEY, + value text NOT NULL, + lifetime integer NOT NULL + );` + + _, err := db.Exec(fmt.Sprintf(stmt, table)) + + return err +} + +// Check if cached key exists in SQL storage +func (s *Sqlite3) Contains(key string) bool { + if _, err := s.Fetch(key); err != nil { + return false + } + + return true +} + +// Delete the cached key from Sqlite3 storage +func (s *Sqlite3) Delete(key string) error { + tx, err := s.db.Begin() + + if err != nil { + return errors.Wrap(err, "Unable to delete") + } + + stmt, err := tx.Prepare( + fmt.Sprintf("DELETE FROM %s WHERE key = ?", s.table), + ) + + if err != nil { + return errors.Wrap(err, "Unable to delete") + } + + defer stmt.Close() + + _, err = stmt.Exec(key) + + if err != nil { + return errors.Wrap(err, "Unable to delete") + } + + tx.Commit() + + return nil +} + +// Retrieve the cached value from key of the Sqlite3 storage +func (s *Sqlite3) Fetch(key string) (string, error) { + stmt, err := s.db.Prepare( + fmt.Sprintf("SELECT value, lifetime FROM %s WHERE key = ?", s.table), + ) + + if err != nil { + return "", errors.Wrap(err, "Unable to retrieve the value") + } + + defer stmt.Close() + + var value string + var lifetime int64 + + err = stmt.QueryRow(key).Scan(&value, &lifetime) + + if err != nil { + return "", errors.Wrap(err, "Unable to retrieve the value") + } + + if lifetime == 0 { + return value, nil + } + + if lifetime <= time.Now().Unix() { + s.Delete(key) + + return "", errors.New("Cache expired") + } + + return value, nil +} + +// Retrieve multiple cached value from keys of the Sqlite3 storage +func (s *Sqlite3) FetchMulti(keys []string) map[string]string { + result := make(map[string]string) + + for _, key := range keys { + if value, err := s.Fetch(key); err == nil { + result[key] = value + } + } + + return result +} + +// Remove all cached keys in Sqlite3 storage +func (s *Sqlite3) Flush() error { + tx, err := s.db.Begin() + + if err != nil { + return errors.Wrap(err, "Unable to flush") + } + + stmt, err := tx.Prepare( + fmt.Sprintf("DELETE FROM %s", s.table), + ) + + if err != nil { + return errors.Wrap(err, "Unable to flush") + } + + defer stmt.Close() + + _, err = stmt.Exec() + + if err != nil { + return errors.Wrap(err, "Unable to flush") + } + + tx.Commit() + + return nil +} + +// Save a value in Sqlite3 storage by key +func (s *Sqlite3) Save(key string, value string, lifeTime time.Duration) error { + duration := int64(0) + + if lifeTime > 0 { + duration = time.Now().Unix() + int64(lifeTime.Seconds()) + } + + tx, err := s.db.Begin() + + if err != nil { + return errors.Wrap(err, "Unable to save") + } + + stmt, err := tx.Prepare( + fmt.Sprintf("INSERT OR REPLACE INTO %s (key, value, lifetime) VALUES (?, ?, ?)", s.table), + ) + + if err != nil { + return errors.Wrap(err, "Unable to save") + } + + defer stmt.Close() + + _, err = stmt.Exec(key, value, duration) + + if err != nil { + return errors.Wrap(err, "Unable to save") + } + + tx.Commit() + + return nil +} diff --git a/sqlite3_test.go b/sqlite3_test.go new file mode 100644 index 0000000..f2c2828 --- /dev/null +++ b/sqlite3_test.go @@ -0,0 +1,190 @@ +package cachego + +import ( + "database/sql" + "fmt" + _ "github.com/mattn/go-sqlite3" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "os" + "testing" + "time" +) + +type Sqlite3TestSuite struct { + suite.Suite + + assert *assert.Assertions + cache Cache + db *sql.DB +} + +var ( + cacheTable string = "cache" + dbPath string = "./cache.db" +) + +func (s *Sqlite3TestSuite) SetupTest() { + + db, err := sql.Open("sqlite3", dbPath) + + if err != nil { + s.T().Skip() + } + + s.cache, err = NewSqlite3(db, cacheTable) + + if err != nil { + s.T().Skip() + } + + s.assert = assert.New(s.T()) + s.db = db +} + +func (s *Sqlite3TestSuite) TearDownTest() { + os.Remove(dbPath) +} + +func (s *Sqlite3TestSuite) TestCreateInstanceThrowAnError() { + s.db.Close() + + _, err := NewSqlite3(s.db, cacheTable) + + s.assert.Error(err) +} + +func (s *Sqlite3TestSuite) TestSaveThrowAnError() { + s.db.Close() + + s.assert.Error(s.cache.Save("foo", "bar", 0)) +} + +func (s *Sqlite3TestSuite) TestSaveThrowAnErrorWhenDropTable() { + s.db.Exec(fmt.Sprintf("DROP TABLE %s;", cacheTable)) + + s.assert.Error(s.cache.Save("foo", "bar", 0)) +} + +func (s *Sqlite3TestSuite) TestSave() { + s.assert.Nil(s.cache.Save("foo", "bar", 0)) +} + +func (s *Sqlite3TestSuite) TestFetchThrowAnError() { + key := "foo" + value := "bar" + + s.cache.Save(key, value, 1) + + result, err := s.cache.Fetch(key) + + s.assert.Error(err) + s.assert.Empty(result) +} + +func (s *Sqlite3TestSuite) TestFetch() { + key := "foo" + value := "bar" + + s.cache.Save(key, value, 0) + + result, err := s.cache.Fetch(key) + + s.assert.Nil(err) + s.assert.Equal(value, result) +} + +func (s *Sqlite3TestSuite) TestFetchWithLongLifetime() { + key := "foo" + value := "bar" + + s.cache.Save(key, value, 10*time.Second) + + result, err := s.cache.Fetch(key) + + s.assert.Nil(err) + s.assert.Equal(value, result) +} + +func (s *Sqlite3TestSuite) TestContainsThrowAnError() { + s.assert.False(s.cache.Contains("bar")) +} + +func (s *Sqlite3TestSuite) TestContains() { + s.cache.Save("foo", "bar", 0) + + s.assert.True(s.cache.Contains("foo")) + s.assert.False(s.cache.Contains("bar")) +} + +func (s *Sqlite3TestSuite) TestDeleteThrowAnError() { + s.db.Close() + + s.assert.Error( + s.cache.Delete("cccc"), + ) +} + +func (s *Sqlite3TestSuite) TestDeleteThrowAnErrorWhenDropTable() { + s.db.Exec(fmt.Sprintf("DROP TABLE %s;", cacheTable)) + + s.assert.Error( + s.cache.Delete("cccc"), + ) +} + +func (s *Sqlite3TestSuite) TestDelete() { + s.cache.Save("foo", "bar", 0) + + s.assert.Nil(s.cache.Delete("foo")) + s.assert.False(s.cache.Contains("foo")) + s.assert.Nil(s.cache.Delete("foo")) +} + +func (s *Sqlite3TestSuite) TestFlushThrowAnError() { + s.db.Close() + + s.assert.Error(s.cache.Flush()) +} + +func (s *Sqlite3TestSuite) TestFlushThrowAnErrorWhenDropTable() { + s.db.Exec(fmt.Sprintf("DROP TABLE %s;", cacheTable)) + + s.assert.Error(s.cache.Flush()) +} + +func (s *Sqlite3TestSuite) TestFlush() { + s.cache.Save("foo", "bar", 0) + + s.assert.Nil(s.cache.Flush()) + s.assert.False(s.cache.Contains("foo")) +} + +func (s *Sqlite3TestSuite) TestFetchMultiReturnNoItemsWhenThrowAnError() { + s.db.Close() + + result := s.cache.FetchMulti([]string{"foo"}) + + s.assert.Len(result, 0) +} + +func (s *Sqlite3TestSuite) TestFetchMulti() { + s.cache.Save("foo", "bar", 0) + s.cache.Save("john", "doe", 0) + + result := s.cache.FetchMulti([]string{"foo", "john"}) + + s.assert.Len(result, 2) +} + +func (s *Sqlite3TestSuite) TestFetchMultiWhenOnlyOneOfKeysExists() { + s.cache.Save("foo", "bar", 0) + + result := s.cache.FetchMulti([]string{"foo", "alice"}) + + s.assert.Len(result, 1) +} + +func TestSqlite3RunSuite(t *testing.T) { + suite.Run(t, new(Sqlite3TestSuite)) +}