diff --git a/.github/workflows/test-postgres.yml b/.github/workflows/test-postgres.yml index 08e8cf41..83cd00b8 100644 --- a/.github/workflows/test-postgres.yml +++ b/.github/workflows/test-postgres.yml @@ -27,9 +27,8 @@ jobs: strategy: matrix: go-version: - - 1.16.x - - 1.18.x - 1.19.x + - 1.20.x platform: - ubuntu-latest - windows-latest diff --git a/postgres/README.md b/postgres/README.md index 60ea5101..a8f777c9 100644 --- a/postgres/README.md +++ b/postgres/README.md @@ -1,6 +1,6 @@ # Postgres -A Postgres storage driver using [lib/pq](https://github.com/lib/pq). +A Postgres storage driver using [jackc/pgx](https://github.com/jackc/pgx). ### Table of Contents - [Signatures](#signatures) @@ -17,7 +17,7 @@ func (s *Storage) Set(key string, val []byte, exp time.Duration) error func (s *Storage) Delete(key string) error func (s *Storage) Reset() error func (s *Storage) Close() error -func (s *Storage) Conn() *sql.DB +func (s *Storage) Conn() *pgxpool.Pool ``` ### Installation Postgres is tested on the 2 last [Go versions](https://golang.org/dl/) with support for modules. So make sure to initialize one first if you didn't do that yet: @@ -26,13 +26,13 @@ go mod init github.com// ``` And then install the postgres implementation: ```bash -go get github.com/gofiber/storage/postgres +go get github.com/gofiber/storage/postgres/v2 ``` ### Examples Import the storage package. ```go -import "github.com/gofiber/storage/postgres" +import "github.com/gofiber/storage/postgres/v2" ``` You can use the following possibilities to create a storage: @@ -42,20 +42,10 @@ store := postgres.New() // Initialize custom config store := postgres.New(postgres.Config{ - Host: "127.0.0.1", - Port: 5432, - Database: "fiber", + Db: dbPool, Table: "fiber_storage", Reset: false, GCInterval: 10 * time.Second, - SslMode: "disable", -}) - -// Initialize custom config using connection string -store := postgres.New(postgres.Config{ - ConnectionURI: "postgresql://user:password@localhost:5432/fiber" - Reset: false, - GCInterval: 10 * time.Second, }) ``` @@ -63,6 +53,11 @@ store := postgres.New(postgres.Config{ ```go // Config defines the config for storage. type Config struct { + // DB pgxpool.Pool object will override connection uri and other connection fields + // + // Optional. Default is nil + DB *pgxpool.Pool + // Connection string to use for DB. Will override all other authentication values if used // // Optional. Default is "" @@ -98,6 +93,11 @@ type Config struct { // Optional. Default is "fiber_storage" Table string + // The SSL mode for the connection + // + // Optional. Default is "disable" + SSLMode string + // Reset clears any existing keys in existing Table // // Optional. Default is false @@ -107,24 +107,20 @@ type Config struct { // // Optional. Default is 10 * time.Second GCInterval time.Duration - - // The SSL mode for the connection - // - // Optional. Default is "disable" - SslMode string } ``` ### Default Config ```go +// ConfigDefault is the default config var ConfigDefault = Config{ - ConnectionURI: "", - Host: "127.0.0.1", - Port: 5432, - Database: "fiber", - Table: "fiber_storage", - Reset: false, - GCInterval: 10 * time.Second, - SslMode: "disable", + ConnectionURI: "", + Host: "127.0.0.1", + Port: 5432, + Database: "fiber", + Table: "fiber_storage", + SSLMode: "disable", + Reset: false, + GCInterval: 10 * time.Second, } ``` diff --git a/postgres/config.go b/postgres/config.go index c9354e4a..f78650fc 100644 --- a/postgres/config.go +++ b/postgres/config.go @@ -1,11 +1,21 @@ package postgres import ( + "fmt" + "net/url" + "strings" "time" + + "github.com/jackc/pgx/v5/pgxpool" ) // Config defines the config for storage. type Config struct { + // DB pgxpool.Pool object will override connection uri and other connection fields + // + // Optional. Default is nil + DB *pgxpool.Pool + // Connection string to use for DB. Will override all other authentication values if used // // Optional. Default is "" @@ -44,7 +54,7 @@ type Config struct { // The SSL mode for the connection // // Optional. Default is "disable" - SslMode string + SSLMode string // Reset clears any existing keys in existing Table // @@ -55,57 +65,50 @@ type Config struct { // // Optional. Default is 10 * time.Second GCInterval time.Duration +} - //////////////////////////////////// - // Adaptor related config options // - //////////////////////////////////// +// ConfigDefault is the default config +var ConfigDefault = Config{ + ConnectionURI: "", + Host: "127.0.0.1", + Port: 5432, + Database: "fiber", + Table: "fiber_storage", + SSLMode: "disable", + Reset: false, + GCInterval: 10 * time.Second, +} - // Maximum wait for connection, in seconds. Zero or - // n < 0 means wait indefinitely. - timeout time.Duration +func (c *Config) getDSN() string { + // Just return ConnectionURI if it's already exists + if c.ConnectionURI != "" { + return c.ConnectionURI + } - // 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 + // Generate DSN + dsn := "postgresql://" + if c.Username != "" { + dsn += url.QueryEscape(c.Username) + } + if c.Password != "" { + dsn += ":" + url.QueryEscape(c.Password) + } + if c.Username != "" || c.Password != "" { + dsn += "@" + } - // 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 + // unix socket host path + if strings.HasPrefix(c.Host, "/") { + dsn += fmt.Sprintf("%s:%d", c.Host, c.Port) + } else { + dsn += fmt.Sprintf("%s:%d", url.QueryEscape(c.Host), c.Port) + } - // 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 -} + dsn += fmt.Sprintf("/%s?sslmode=%s", + url.QueryEscape(c.Database), + c.SSLMode) -// ConfigDefault is the default config -var ConfigDefault = Config{ - ConnectionURI: "", - Host: "127.0.0.1", - Port: 5432, - Database: "fiber", - Table: "fiber_storage", - SslMode: "disable", - Reset: false, - GCInterval: 10 * time.Second, - maxOpenConns: 100, - maxIdleConns: 100, - connMaxLifetime: 1 * time.Second, + return dsn } // Helper function to set default values @@ -114,7 +117,6 @@ func configDefault(config ...Config) Config { if len(config) < 1 { return ConfigDefault } - // Override default config cfg := config[0] @@ -131,8 +133,8 @@ func configDefault(config ...Config) Config { if cfg.Table == "" { cfg.Table = ConfigDefault.Table } - if cfg.SslMode == "" { - cfg.SslMode = ConfigDefault.SslMode + if cfg.Table == "" { + cfg.Table = ConfigDefault.Table } if int(cfg.GCInterval.Seconds()) <= 0 { cfg.GCInterval = ConfigDefault.GCInterval diff --git a/postgres/go.mod b/postgres/go.mod index 4f7d0016..cb053a99 100644 --- a/postgres/go.mod +++ b/postgres/go.mod @@ -1,8 +1,17 @@ -module github.com/gofiber/storage/postgres +module github.com/gofiber/storage/postgres/v2 -go 1.16 +go 1.19 require ( github.com/gofiber/utils v1.0.1 - github.com/lib/pq v1.10.7 + github.com/jackc/pgx/v5 v5.3.1 +) + +require ( + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/puddle/v2 v2.2.0 // indirect + golang.org/x/crypto v0.7.0 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/text v0.8.0 // indirect ) diff --git a/postgres/go.sum b/postgres/go.sum index 3311f189..390bc2e3 100644 --- a/postgres/go.sum +++ b/postgres/go.sum @@ -1,4 +1,27 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/gofiber/utils v1.0.1 h1:knct4cXwBipWQqFrOy1Pv6UcgPM+EXo9jDgc66V1Qio= github.com/gofiber/utils v1.0.1/go.mod h1:pacRFtghAE3UoknMOUiXh2Io/nLWSUHtQCi/3QASsOc= -github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= -github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU= +github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= +github.com/jackc/puddle/v2 v2.2.0 h1:RdcDk92EJBuBS55nQMMYFXTxwstHug4jkhT5pq8VxPk= +github.com/jackc/puddle/v2 v2.2.0/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/postgres/postgres.go b/postgres/postgres.go index b08d8576..e995e954 100644 --- a/postgres/postgres.go +++ b/postgres/postgres.go @@ -1,19 +1,20 @@ package postgres import ( - "database/sql" + "context" "errors" "fmt" - "net/url" + "os" "strings" "time" - _ "github.com/lib/pq" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" ) // Storage interface that is implemented by storage providers type Storage struct { - db *sql.DB + db *pgxpool.Pool gcInterval time.Duration done chan struct{} @@ -45,62 +46,33 @@ func New(config ...Config) *Storage { // Set default config cfg := configDefault(config...) - // Create data source name - var dsn string - if cfg.ConnectionURI != "" { - dsn = cfg.ConnectionURI - } else { - dsn = "postgresql://" - if cfg.Username != "" { - dsn += url.QueryEscape(cfg.Username) + // Select db connection + var err error + db := cfg.DB + if db == nil { + db, err = pgxpool.New(context.Background(), cfg.getDSN()) + if err != nil { + fmt.Fprintf(os.Stderr, "Unable to create connection pool: %v\n", err) } - if cfg.Password != "" { - dsn += ":" + url.QueryEscape(cfg.Password) - } - if cfg.Username != "" || cfg.Password != "" { - dsn += "@" - } - // unix socket host path - if strings.HasPrefix(cfg.Host, "/") { - dsn += fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) - } else { - dsn += fmt.Sprintf("%s:%d", url.QueryEscape(cfg.Host), cfg.Port) - } - dsn += fmt.Sprintf("/%s?connect_timeout=%d&sslmode=%s", - url.QueryEscape(cfg.Database), - int64(cfg.timeout.Seconds()), - cfg.SslMode) } - // 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 { + if err := db.Ping(context.Background()); err != nil { panic(err) } // Drop table if set to true if cfg.Reset { - if _, err = db.Exec(fmt.Sprintf(dropQuery, cfg.Table)); err != nil { - _ = db.Close() + if _, err := db.Exec(context.Background(), fmt.Sprintf(dropQuery, cfg.Table)); err != nil { + db.Close() panic(err) } } // Init database queries for _, query := range initQuery { - if _, err := db.Exec(fmt.Sprintf(query, cfg.Table)); err != nil { - _ = db.Close() - + if _, err := db.Exec(context.Background(), fmt.Sprintf(query, cfg.Table)); err != nil { + db.Close() panic(err) } } @@ -125,21 +97,19 @@ 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) { if len(key) <= 0 { return nil, nil } - row := s.db.QueryRow(s.sqlSelect, key) + row := s.db.QueryRow(context.Background(), s.sqlSelect, key) // Add db response to data var ( - data = []byte{} + data []byte exp int64 = 0 ) if err := row.Scan(&data, &exp); err != nil { - if err == sql.ErrNoRows { + if errors.Is(err, pgx.ErrNoRows) { return nil, nil } return nil, err @@ -163,7 +133,7 @@ func (s *Storage) Set(key string, val []byte, exp time.Duration) error { if exp != 0 { expSeconds = time.Now().Add(exp).Unix() } - _, err := s.db.Exec(s.sqlInsert, key, val, expSeconds, val, expSeconds) + _, err := s.db.Exec(context.Background(), s.sqlInsert, key, val, expSeconds, val, expSeconds) return err } @@ -173,24 +143,26 @@ func (s *Storage) Delete(key string) error { if len(key) <= 0 { return nil } - _, err := s.db.Exec(s.sqlDelete, key) + _, err := s.db.Exec(context.Background(), s.sqlDelete, key) return err } // Reset all entries, including unexpired func (s *Storage) Reset() error { - _, err := s.db.Exec(s.sqlReset) + _, err := s.db.Exec(context.Background(), s.sqlReset) return err } // Close the database func (s *Storage) Close() error { s.done <- struct{}{} - return s.db.Close() + s.db.Stat() + s.db.Close() + return nil } // Return database client -func (s *Storage) Conn() *sql.DB { +func (s *Storage) Conn() *pgxpool.Pool { return s.db } @@ -210,13 +182,13 @@ func (s *Storage) gcTicker() { // gc deletes all expired entries func (s *Storage) gc(t time.Time) { - _, _ = s.db.Exec(s.sqlGC, t.Unix()) + _, _ = s.db.Exec(context.Background(), s.sqlGC, t.Unix()) } func (s *Storage) checkSchema(tableName string) { var data []byte - row := s.db.QueryRow(fmt.Sprintf(checkSchemaQuery, tableName)) + row := s.db.QueryRow(context.Background(), fmt.Sprintf(checkSchemaQuery, tableName)) if err := row.Scan(&data); err != nil { panic(err) } diff --git a/postgres/postgres_test.go b/postgres/postgres_test.go index c2af76bd..fddf0223 100644 --- a/postgres/postgres_test.go +++ b/postgres/postgres_test.go @@ -1,12 +1,13 @@ package postgres import ( - "database/sql" + "context" "os" "testing" "time" "github.com/gofiber/utils" + "github.com/jackc/pgx/v5" ) var testStore = New(Config{ @@ -133,9 +134,9 @@ func Test_Postgres_GC(t *testing.T) { utils.AssertEqual(t, nil, err) testStore.gc(time.Now()) - row := testStore.db.QueryRow(testStore.sqlSelect, "john") + row := testStore.db.QueryRow(context.Background(), testStore.sqlSelect, "john") err = row.Scan(nil, nil) - utils.AssertEqual(t, sql.ErrNoRows, err) + utils.AssertEqual(t, pgx.ErrNoRows, err) // This key should not expire err = testStore.Set("john", testVal, 0) @@ -166,18 +167,14 @@ func Test_SslRequiredMode(t *testing.T) { } }() _ = New(Config{ - Database: "fiber", - Username: "username", - Password: "password", - Reset: true, - SslMode: "require", + Reset: true, }) } -func Test_Postgres_Close(t *testing.T) { - utils.AssertEqual(t, nil, testStore.Close()) -} - func Test_Postgres_Conn(t *testing.T) { utils.AssertEqual(t, true, testStore.Conn() != nil) } + +func Test_Postgres_Close(t *testing.T) { + utils.AssertEqual(t, nil, testStore.Close()) +}