📦 postgres

Co-Authored-By: Alireza Salary <arsmn1996@gmail.com>
This commit is contained in:
Fenny
2020-11-01 12:25:26 +01:00
parent 127aa2a67d
commit dd72caf629
9 changed files with 230 additions and 20 deletions

6
.github/README.md vendored
View File

@@ -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?

1
go.mod
View File

@@ -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
)

2
go.sum
View File

@@ -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=

View File

@@ -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
}

View File

@@ -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)
}
}
}

View File

@@ -18,6 +18,7 @@ type Config struct {
// The network type, either tcp or unix.
// Default is tcp.
Network string
// host:port address.
Addr string

View File

@@ -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

View File

@@ -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
}

View File

@@ -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))