diff --git a/.github/README.md b/.github/README.md index b17200c5..11a57331 100644 --- a/.github/README.md +++ b/.github/README.md @@ -10,10 +10,10 @@ Premade storage drivers that implement [`fiber.Storage`](https://github.com/gofi * [Memcached](/memcached) **(UNFINISHED)** * [Memory](/memory) * [MongoDB](/mongodb) -* [MySQL](/mysql) **(UNFINISHED)** -* [Postgres](/postgres) **(UNFINISHED)** +* [MySQL](/mysql) **(UNFINISHED)** +* [Postgres](/postgres) * [Redis](/redis) -* [SQLite3](/sqlite3) **(UNFINISHED)** +* [SQLite3](/sqlite3) ## 🤔 Something missing? diff --git a/go.mod b/go.mod index 7c68f118..ddc0bac5 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.14 require ( github.com/go-redis/redis/v8 v8.3.3 github.com/gofiber/utils v0.1.0 + github.com/lib/pq v1.8.0 github.com/mattn/go-sqlite3 v1.14.4 go.mongodb.org/mongo-driver v1.4.2 ) diff --git a/go.sum b/go.sum index 9b771af4..5e12e9fe 100644 --- a/go.sum +++ b/go.sum @@ -78,6 +78,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg= +github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/mattn/go-sqlite3 v1.14.4 h1:4rQjbDxdu9fSgI/r3KN72G3c2goxknAqHHgPWWs8UlI= diff --git a/postgres/config.go b/postgres/config.go index 30051419..131c8d30 100644 --- a/postgres/config.go +++ b/postgres/config.go @@ -4,12 +4,91 @@ import "time" // Config defines the config for storage. type Config struct { + // Time before deleting expired keys + // + // Default is 10 * time.Second GCInterval time.Duration + + // DB host + // + // Optional. Default is "127.0.0.1" + Host string + + // DB port + // + // Optional. Default is "5432" + Port int64 + + // DB user name + // + // Optional. Default is "" + Username string + + // DB user password + // + // Optional. Default is "" + Password string + + // DB name + // + // Optional. Default is "fiber" + Database string + + // DB table name + // + // Optional. Default is "fiber" + TableName string + + // Drop any existing table with the same name + // + // Optional. Default is false + DropTable bool + + // Maximum wait for connection, in seconds. Zero or + // n < 0 means wait indefinitely. + Timeout time.Duration + + // The maximum number of connections in the idle connection pool. + // + // If MaxOpenConns is greater than 0 but less than the new MaxIdleConns, + // then the new MaxIdleConns will be reduced to match the MaxOpenConns limit. + // + // If n <= 0, no idle connections are retained. + // + // The default max idle connections is currently 2. This may change in + // a future release. + MaxIdleConns int + + // The maximum number of open connections to the database. + // + // If MaxIdleConns is greater than 0 and the new MaxOpenConns is less than + // MaxIdleConns, then MaxIdleConns will be reduced to match the new + // MaxOpenConns limit. + // + // If n <= 0, then there is no limit on the number of open connections. + // The default is 0 (unlimited). + MaxOpenConns int + + // The maximum amount of time a connection may be reused. + // + // Expired connections may be closed lazily before reuse. + // + // If d <= 0, connections are reused forever. + ConnMaxLifetime time.Duration } // ConfigDefault is the default config var ConfigDefault = Config{ - GCInterval: 10 * time.Second, + GCInterval: 10 * time.Second, + Host: "127.0.0.1", + Port: 5432, + Database: "fiber", + TableName: "fiber", + DropTable: false, + Timeout: 30 * time.Second, + MaxOpenConns: 100, + MaxIdleConns: 100, + ConnMaxLifetime: 1 * time.Second, } // Helper function to set default values @@ -26,5 +105,32 @@ func configDefault(config ...Config) Config { if int(cfg.GCInterval) == 0 { cfg.GCInterval = ConfigDefault.GCInterval } + if cfg.Host == "" { + cfg.Host = ConfigDefault.Host + } + if cfg.Port == 0 { + cfg.Port = ConfigDefault.Port + } + if cfg.Host == "" { + cfg.Host = ConfigDefault.Host + } + if cfg.Database == "" { + cfg.Database = ConfigDefault.Database + } + if cfg.TableName == "" { + cfg.TableName = ConfigDefault.TableName + } + if int(cfg.Timeout) == 0 { + cfg.Timeout = ConfigDefault.Timeout + } + if cfg.MaxOpenConns == 0 { + cfg.MaxOpenConns = ConfigDefault.MaxOpenConns + } + if cfg.MaxIdleConns == 0 { + cfg.MaxIdleConns = ConfigDefault.MaxIdleConns + } + if int(cfg.ConnMaxLifetime) == 0 { + cfg.ConnMaxLifetime = ConfigDefault.ConnMaxLifetime + } return cfg } diff --git a/postgres/postgres.go b/postgres/postgres.go index 0d871098..afb665ed 100644 --- a/postgres/postgres.go +++ b/postgres/postgres.go @@ -1,22 +1,96 @@ package postgres import ( + "database/sql" + "errors" + "fmt" + "net/url" "time" + + "github.com/gofiber/utils" + _ "github.com/lib/pq" ) // Storage interface that is implemented by storage providers type Storage struct { + db *sql.DB gcInterval time.Duration + + sqlSelect string + sqlInsert string + sqlDelete string + sqlClear string + sqlGC string } +var ( + dropQuery = `DROP TABLE IF EXISTS %s;` + initQuery = []string{ + `CREATE TABLE IF NOT EXISTS %s ( + key VARCHAR(64) PRIMARY KEY NOT NULL DEFAULT '', + data TEXT NOT NULL, + exp BIGINT NOT NULL DEFAULT '0' + );`, + `CREATE INDEX IF NOT EXISTS exp ON %s (exp);`, + } +) + // New creates a new storage func New(config ...Config) *Storage { // Set default config cfg := configDefault(config...) + // Create data source name + dsn := fmt.Sprintf("postgresql://%s:%s@%s:%d/%s?connect_timeout=%d&sslmode=disable", + url.QueryEscape(cfg.Username), + cfg.Password, + url.QueryEscape(cfg.Host), + cfg.Port, + url.QueryEscape(cfg.Database), + int64(cfg.Timeout.Seconds())) + + // Create db + db, err := sql.Open("postgres", dsn) + if err != nil { + panic(err) + } + + // Set database options + db.SetMaxOpenConns(cfg.MaxOpenConns) + db.SetMaxIdleConns(cfg.MaxIdleConns) + db.SetConnMaxLifetime(cfg.ConnMaxLifetime) + + // Ping database + if err := db.Ping(); err != nil { + panic(err) + } + + // Drop table if set to true + if cfg.DropTable { + if _, err = db.Exec(fmt.Sprintf(dropQuery, cfg.TableName)); err != nil { + _ = db.Close() + panic(err) + } + } + + // Init database queries + for _, query := range initQuery { + if _, err := db.Exec(fmt.Sprintf(query, cfg.TableName)); err != nil { + _ = db.Close() + fmt.Println(fmt.Sprintf(query, cfg.TableName)) + panic(err) + } + } + // Create storage store := &Storage{ + db: db, gcInterval: cfg.GCInterval, + sqlSelect: fmt.Sprintf(`SELECT data, exp FROM %s WHERE key=$1;`, cfg.TableName), + sqlInsert: fmt.Sprintf("INSERT INTO %s (key, data, exp) VALUES ($1, $2, $3)", cfg.TableName), + sqlDelete: fmt.Sprintf("DELETE FROM %s WHERE key=$1", cfg.TableName), + sqlClear: fmt.Sprintf("DELETE FROM %s;", cfg.TableName), + sqlGC: fmt.Sprintf("DELETE FROM %s WHERE exp <= $1", cfg.TableName), } // Start garbage collector @@ -25,31 +99,57 @@ func New(config ...Config) *Storage { return store } +var noRows = errors.New("sql: no rows in result set") + // Get value by key func (s *Storage) Get(key string) ([]byte, error) { - return nil, nil + row := s.db.QueryRow(s.sqlSelect, key) + + // Add db response to data + var ( + data = []byte{} + exp int64 = 0 + ) + if err := row.Scan(&data, &exp); err != nil { + if err != noRows { + return nil, err + } + return nil, nil + } + + // If the expiration time has already passed, then return nil + if time.Now().After(time.Unix(exp, 0)) { + return nil, nil + } + + return data, nil } // Set key with value func (s *Storage) Set(key string, val []byte, exp time.Duration) error { - return nil + _, err := s.db.Exec(s.sqlInsert, key, utils.UnsafeString(val), time.Now().Add(exp).Unix()) + return err } -// Delete key by key +// Delete entry by key func (s *Storage) Delete(key string) error { - return nil + _, err := s.db.Exec(s.sqlDelete, key) + return err } -// Clear all keys +// Clear all entries, including unexpired func (s *Storage) Clear() error { - return nil + _, err := s.db.Exec(s.sqlClear) + return err } -// Garbage collector to delete expired keys +// GC deletes all expired entries func (s *Storage) gc() { tick := time.NewTicker(s.gcInterval) for { <-tick.C - // clean entries + if _, err := s.db.Exec(s.sqlGC); err != nil { + panic(err) + } } } diff --git a/redis/config.go b/redis/config.go index c396a734..249da135 100644 --- a/redis/config.go +++ b/redis/config.go @@ -18,6 +18,7 @@ type Config struct { // The network type, either tcp or unix. // Default is tcp. Network string + // host:port address. Addr string diff --git a/sqlite3/config.go b/sqlite3/config.go index 3ed6b20b..f7bff760 100644 --- a/sqlite3/config.go +++ b/sqlite3/config.go @@ -12,7 +12,7 @@ type Config struct { // DB file path // // Default is "./fiber.sqlite3" - FilePath string + Database string // DB table name // @@ -56,7 +56,7 @@ type Config struct { // ConfigDefault is the default config var ConfigDefault = Config{ GCInterval: 10 * time.Second, - FilePath: "./fiber.sqlite3", + Database: "./fiber.sqlite3", TableName: "fiber", DropTable: false, MaxOpenConns: 100, @@ -78,8 +78,8 @@ func configDefault(config ...Config) Config { if int(cfg.GCInterval) == 0 { cfg.GCInterval = ConfigDefault.GCInterval } - if cfg.FilePath == "" { - cfg.FilePath = ConfigDefault.FilePath + if cfg.Database == "" { + cfg.Database = ConfigDefault.Database } if cfg.TableName == "" { cfg.TableName = ConfigDefault.TableName diff --git a/sqlite3/sqlite3.go b/sqlite3/sqlite3.go index a448fdf2..c0d6edf3 100644 --- a/sqlite3/sqlite3.go +++ b/sqlite3/sqlite3.go @@ -41,7 +41,7 @@ func New(config ...Config) *Storage { cfg := configDefault(config...) // Create db - db, err := sql.Open("sqlite3", cfg.FilePath) + db, err := sql.Open("sqlite3", cfg.Database) if err != nil { panic(err) } @@ -118,7 +118,7 @@ func (s *Storage) Get(key string) ([]byte, error) { // Set key with value func (s *Storage) Set(key string, val []byte, exp time.Duration) error { - _, err := s.db.Exec(s.sqlInsert, key, utils.GetString(val), time.Now().Add(exp).Unix()) + _, err := s.db.Exec(s.sqlInsert, key, utils.UnsafeString(val), time.Now().Add(exp).Unix()) return err } diff --git a/sqlite3/sqlite3_test.go b/sqlite3/sqlite3_test.go index b75e7353..c67ddfbe 100644 --- a/sqlite3/sqlite3_test.go +++ b/sqlite3/sqlite3_test.go @@ -14,7 +14,7 @@ func Test_New(t *testing.T) { func Test_Get_Set(t *testing.T) { s := New(Config{ - FilePath: ":memory:", + Database: ":memory:", }) err := s.Set("fiber-10k-stars?", []byte("yes!"), time.Duration(time.Hour*1)) utils.AssertEqual(t, nil, err) @@ -26,7 +26,7 @@ func Test_Get_Set(t *testing.T) { func Test_Expiration(t *testing.T) { s := New(Config{ - FilePath: ":memory:", + Database: ":memory:", }) err := s.Set("fiber-20k-stars?", []byte("yes!"), time.Duration(time.Nanosecond/2))