From 1f2c29fd9a51f59f0df0e1b38d0d316ac6393ece Mon Sep 17 00:00:00 2001 From: gandaldf Date: Tue, 25 Oct 2022 17:08:35 +0200 Subject: [PATCH] Add support to MSSQL --- go.mod | 5 + go.sum | 51 ++++++++++ mssql/README.md | 135 +++++++++++++++++++++++++ mssql/config.go | 146 +++++++++++++++++++++++++++ mssql/go.mod | 8 ++ mssql/go.sum | 40 ++++++++ mssql/mssql.go | 235 ++++++++++++++++++++++++++++++++++++++++++++ mssql/mssql_test.go | 183 ++++++++++++++++++++++++++++++++++ 8 files changed, 803 insertions(+) create mode 100644 mssql/README.md create mode 100644 mssql/config.go create mode 100644 mssql/go.mod create mode 100644 mssql/go.sum create mode 100644 mssql/mssql.go create mode 100644 mssql/mssql_test.go diff --git a/go.mod b/go.mod index 80d16029..a2c15388 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,8 @@ module github.com/gofiber/storage go 1.14 + +require ( + github.com/denisenkom/go-mssqldb v0.12.3 + github.com/gofiber/utils v1.0.0 +) diff --git a/go.sum b/go.sum index e69de29b..7a0388af 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,51 @@ +github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0= +github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8= +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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw= +github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/gofiber/utils v1.0.0 h1:goxlTmYidOhsCvuZuTLzT224DELpnz9c/+iw5eN9FJw= +github.com/gofiber/utils v1.0.0/go.mod h1:RYennBgjLkuNtU+dxg7QgBEU8tmixFQHQ2GE1ioZlxw= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= +github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= +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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/mssql/README.md b/mssql/README.md new file mode 100644 index 00000000..39fbf241 --- /dev/null +++ b/mssql/README.md @@ -0,0 +1,135 @@ +# MSSQL + +A MSSQL storage driver using [denisenkom/go-mssqldb](https://github.com/denisenkom/go-mssqldb). + +### Table of Contents +- [Signatures](#signatures) +- [Installation](#installation) +- [Examples](#examples) +- [Config](#config) +- [Default Config](#default-config) + +### Signatures +```go +func New(config ...Config) Storage +func (s *Storage) Get(key string) ([]byte, error) +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 +``` +### Installation +MSSQL 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: +```bash +go mod init github.com// +``` +And then install the mssql implementation: +```bash +go get github.com/gofiber/storage/mssql +``` + +### Examples +Import the storage package. +```go +import "github.com/gofiber/storage/mssql" +``` + +You can use the following possibilities to create a storage: +```go +// Initialize default config +store := mssql.New() + +// Initialize custom config +store := mssql.New(mssql.Config{ + Host: "127.0.0.1", + Port: 1433, + Database: "fiber", + Table: "fiber_storage", + Reset: false, + GCInterval: 10 * time.Second, + SslMode: "disable", +}) + +// Initialize custom config using connection string +store := mssql.New(mssql.Config{ + ConnectionURI: "sqlserver://user:password@localhost:1433?database=fiber" + Reset: false, + GCInterval: 10 * time.Second, +}) +``` + +### Config +```go +// Config defines the config for storage. +type Config struct { + // Connection string to use for DB. Will override all other authentication values if used + // + // Optional. Default is "" + ConnectionURI string + + // Host name where the DB is hosted + // + // Optional. Default is "127.0.0.1" + Host string + + // Port where the DB is listening on + // + // Optional. Default is 1433 + Port int + + // Server username + // + // Optional. Default is "" + Username string + + // Server password + // + // Optional. Default is "" + Password string + + // Instance name + // + // Optional. Default is "" + Instance string + + // Database name + // + // Optional. Default is "fiber" + Database string + + // Table name + // + // Optional. Default is "fiber_storage" + Table string + + // Reset clears any existing keys in existing Table + // + // Optional. Default is false + Reset bool + + // Time before deleting expired keys + // + // Optional. Default is 10 * time.Second + GCInterval time.Duration + + // The SSL mode for the connection + // + // Optional. Default is "disable" + SslMode string +} +``` + +### Default Config +```go +var ConfigDefault = Config{ + ConnectionURI: "", + Host: "127.0.0.1", + Port: 1433, + Database: "fiber", + Table: "fiber_storage", + Reset: false, + GCInterval: 10 * time.Second, + SslMode: "disable", +} +``` diff --git a/mssql/config.go b/mssql/config.go new file mode 100644 index 00000000..9b0cef44 --- /dev/null +++ b/mssql/config.go @@ -0,0 +1,146 @@ +package mssql + +import ( + "time" +) + +// Config defines the config for storage. +type Config struct { + // Connection string to use for DB. Will override all other authentication values if used + // + // Optional. Default is "" + ConnectionURI string + + // Host name where the DB is hosted + // + // Optional. Default is "127.0.0.1" + Host string + + // Port where the DB is listening on + // + // Optional. Default is 1433 + Port int + + // Server username + // + // Optional. Default is "" + Username string + + // Server password + // + // Optional. Default is "" + Password string + + // Instance name + // + // Optional. Default is "" + Instance string + + // Database name + // + // Optional. Default is "fiber" + Database string + + // Table name + // + // 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 + Reset bool + + // Time before deleting expired keys + // + // Optional. Default is 10 * time.Second + GCInterval time.Duration + + //////////////////////////////////// + // Adaptor related config options // + //////////////////////////////////// + + // 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{ + ConnectionURI: "", + Host: "127.0.0.1", + Port: 1433, + Database: "fiber", + Table: "fiber_storage", + SslMode: "disable", + Reset: false, + GCInterval: 10 * time.Second, + maxOpenConns: 100, + maxIdleConns: 100, + connMaxLifetime: 1 * time.Second, +} + +// Helper function to set default values +func configDefault(config ...Config) Config { + // Return default config if nothing provided + if len(config) < 1 { + return ConfigDefault + } + + // Override default config + cfg := config[0] + + // Set default values + if cfg.Host == "" { + cfg.Host = ConfigDefault.Host + } + if cfg.Port <= 0 { + cfg.Port = ConfigDefault.Port + } + if cfg.Database == "" { + cfg.Database = ConfigDefault.Database + } + if cfg.Table == "" { + cfg.Table = ConfigDefault.Table + } + if cfg.SslMode == "" { + cfg.SslMode = ConfigDefault.SslMode + } + if int(cfg.GCInterval.Seconds()) <= 0 { + cfg.GCInterval = ConfigDefault.GCInterval + } + return cfg +} diff --git a/mssql/go.mod b/mssql/go.mod new file mode 100644 index 00000000..a73a2fa2 --- /dev/null +++ b/mssql/go.mod @@ -0,0 +1,8 @@ +module github.com/gofiber/storage/mssql + +go 1.14 + +require ( + github.com/denisenkom/go-mssqldb v0.12.3 + github.com/gofiber/utils v0.1.2 +) diff --git a/mssql/go.sum b/mssql/go.sum new file mode 100644 index 00000000..bac800e1 --- /dev/null +++ b/mssql/go.sum @@ -0,0 +1,40 @@ +github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0= +github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw= +github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/gofiber/utils v0.1.2 h1:1SH2YEz4RlNS0tJlMJ0bGwO0JkqPqvq6TbHK9tXZKtk= +github.com/gofiber/utils v0.1.2/go.mod h1:pacRFtghAE3UoknMOUiXh2Io/nLWSUHtQCi/3QASsOc= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= +github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= +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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/mssql/mssql.go b/mssql/mssql.go new file mode 100644 index 00000000..2b810b4a --- /dev/null +++ b/mssql/mssql.go @@ -0,0 +1,235 @@ +package mssql + +import ( + "database/sql" + "errors" + "fmt" + "net/url" + "strings" + "time" + + _ "github.com/denisenkom/go-mssqldb" +) + +// Storage interface that is implemented by storage providers +type Storage struct { + db *sql.DB + gcInterval time.Duration + done chan struct{} + + sqlSelect string + sqlInsert string + sqlDelete string + sqlReset string + sqlGC string +} + +var ( + checkSchemaMsg = "The `v` row has an incorrect data type. " + + "It should be VARBINARY(MAX) but is instead %s. This will cause encoding-related panics if the DB is not migrated (see https://github.com/gofiber/storage/blob/main/MIGRATE.md)." + dropQuery = `IF EXISTS(SELECT * FROM sys.tables WHERE name = '%s') + DROP TABLE %s;` + initQuery = []string{ + `IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = '%s') + CREATE TABLE %s ( + k VARCHAR(64) PRIMARY KEY NOT NULL DEFAULT '', + v VARBINARY(MAX) NOT NULL, + e BIGINT NOT NULL DEFAULT '0' + );`, + `IF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'e') + CREATE INDEX e ON %s (e);`, + } + checkSchemaQuery = `SELECT DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS + WHERE table_name = '%s' AND COLUMN_NAME = 'v';` +) + +// New creates a new storage +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 = "sqlserver://" + if cfg.Username != "" { + dsn += url.QueryEscape(cfg.Username) + } + if cfg.Password != "" { + dsn += ":" + 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) + } + if cfg.Instance != "" { + dsn += "/" + cfg.Instance + } + dsn += fmt.Sprintf("?database=%s&connection+timeout=%d&encrypt=%s", + url.QueryEscape(cfg.Database), + int64(cfg.timeout.Seconds()), + cfg.SslMode) + } + + // Create db + db, err := sql.Open("sqlserver", 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.Reset { + if _, err = db.Exec(strings.Replace(dropQuery, "%s", cfg.Table, -1)); err != nil { + _ = db.Close() + panic(err) + } + } + + // Init database queries + for _, query := range initQuery { + if _, err := db.Exec(strings.Replace(query, "%s", cfg.Table, -1)); err != nil { + _ = db.Close() + + panic(err) + } + } + + // Create storage + store := &Storage{ + db: db, + gcInterval: cfg.GCInterval, + done: make(chan struct{}), + sqlSelect: fmt.Sprintf(`SELECT v, e FROM %s WHERE k=@p1;`, cfg.Table), + sqlInsert: fmt.Sprintf(`MERGE INTO %s WITH (HOLDLOCK) AS T USING (VALUES(@p1)) AS S (k) ON (T.k = S.k) + WHEN MATCHED THEN UPDATE SET v = @p2, e = @p3 + WHEN NOT MATCHED THEN INSERT (k, v, e) VALUES(@p1, @p2, @p3);`, cfg.Table), + sqlDelete: fmt.Sprintf("DELETE FROM %s WHERE k=@p1", cfg.Table), + sqlReset: fmt.Sprintf("TRUNCATE TABLE %s;", cfg.Table), + sqlGC: fmt.Sprintf("DELETE FROM %s WHERE e <= @p1 AND e != 0", cfg.Table), + } + + store.checkSchema(cfg.Table) + + // Start garbage collector + go store.gcTicker() + + 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) + // Add db response to data + var ( + data = []byte{} + exp int64 = 0 + ) + if err := row.Scan(&data, &exp); err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, err + } + + // If the expiration time has already passed, then return nil + if exp != 0 && exp <= time.Now().Unix() { + return nil, nil + } + + return data, nil +} + +// Set key with value +func (s *Storage) Set(key string, val []byte, exp time.Duration) error { + // Ain't Nobody Got Time For That + if len(key) <= 0 || len(val) <= 0 { + return nil + } + var expSeconds int64 + if exp != 0 { + expSeconds = time.Now().Add(exp).Unix() + } + _, err := s.db.Exec(s.sqlInsert, key, val, expSeconds) + return err +} + +// Delete entry by key +func (s *Storage) Delete(key string) error { + // Ain't Nobody Got Time For That + if len(key) <= 0 { + return nil + } + _, err := s.db.Exec(s.sqlDelete, key) + return err +} + +// Reset all entries, including unexpired +func (s *Storage) Reset() error { + _, err := s.db.Exec(s.sqlReset) + return err +} + +// Close the database +func (s *Storage) Close() error { + s.done <- struct{}{} + return s.db.Close() +} + +// Return database client +func (s *Storage) Conn() *sql.DB { + return s.db +} + +// gcTicker starts the gc ticker +func (s *Storage) gcTicker() { + ticker := time.NewTicker(s.gcInterval) + defer ticker.Stop() + for { + select { + case <-s.done: + return + case t := <-ticker.C: + s.gc(t) + } + } +} + +// gc deletes all expired entries +func (s *Storage) gc(t time.Time) { + _, _ = s.db.Exec(s.sqlGC, t.Unix()) +} + +func (s *Storage) checkSchema(tableName string) { + var data []byte + + row := s.db.QueryRow(fmt.Sprintf(checkSchemaQuery, tableName)) + if err := row.Scan(&data); err != nil { + panic(err) + } + + if strings.ToLower(string(data)) != "varbinary" { + fmt.Printf(checkSchemaMsg, string(data)) + } +} diff --git a/mssql/mssql_test.go b/mssql/mssql_test.go new file mode 100644 index 00000000..27450252 --- /dev/null +++ b/mssql/mssql_test.go @@ -0,0 +1,183 @@ +package mssql + +import ( + "database/sql" + "os" + "testing" + "time" + + "github.com/gofiber/utils" +) + +var testStore = New(Config{ + Database: os.Getenv("MSSQL_DATABASE"), + Username: os.Getenv("MSSQL_USERNAME"), + Password: os.Getenv("MSSQL_PASSWORD"), + Reset: true, +}) + +func Test_MSSQL_Set(t *testing.T) { + var ( + key = "john" + val = []byte("doe") + ) + + err := testStore.Set(key, val, 0) + utils.AssertEqual(t, nil, err) +} + +func Test_MSSQL_Set_Override(t *testing.T) { + var ( + key = "john" + val = []byte("doe") + ) + + err := testStore.Set(key, val, 0) + utils.AssertEqual(t, nil, err) + + err = testStore.Set(key, val, 0) + utils.AssertEqual(t, nil, err) +} + +func Test_MSSQL_Get(t *testing.T) { + var ( + key = "john" + val = []byte("doe") + ) + + err := testStore.Set(key, val, 0) + utils.AssertEqual(t, nil, err) + + result, err := testStore.Get(key) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, val, result) +} + +func Test_MSSQL_Set_Expiration(t *testing.T) { + var ( + key = "john" + val = []byte("doe") + exp = 1 * time.Second + ) + + err := testStore.Set(key, val, exp) + utils.AssertEqual(t, nil, err) + + time.Sleep(1100 * time.Millisecond) +} + +func Test_MSSQL_Get_Expired(t *testing.T) { + var ( + key = "john" + ) + + result, err := testStore.Get(key) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, len(result) == 0) +} + +func Test_MSSQL_Get_NotExist(t *testing.T) { + + result, err := testStore.Get("notexist") + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, len(result) == 0) +} + +func Test_MSSQL_Delete(t *testing.T) { + var ( + key = "john" + val = []byte("doe") + ) + + err := testStore.Set(key, val, 0) + utils.AssertEqual(t, nil, err) + + err = testStore.Delete(key) + utils.AssertEqual(t, nil, err) + + result, err := testStore.Get(key) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, len(result) == 0) +} + +func Test_MSSQL_Reset(t *testing.T) { + var ( + val = []byte("doe") + ) + + err := testStore.Set("john1", val, 0) + utils.AssertEqual(t, nil, err) + + err = testStore.Set("john2", val, 0) + utils.AssertEqual(t, nil, err) + + err = testStore.Reset() + utils.AssertEqual(t, nil, err) + + result, err := testStore.Get("john1") + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, len(result) == 0) + + result, err = testStore.Get("john2") + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, len(result) == 0) +} + +func Test_MSSQL_GC(t *testing.T) { + var ( + testVal = []byte("doe") + ) + + // This key should expire + err := testStore.Set("john", testVal, time.Nanosecond) + utils.AssertEqual(t, nil, err) + + testStore.gc(time.Now()) + row := testStore.db.QueryRow(testStore.sqlSelect, "john") + err = row.Scan(nil, nil) + utils.AssertEqual(t, sql.ErrNoRows, err) + + // This key should not expire + err = testStore.Set("john", testVal, 0) + utils.AssertEqual(t, nil, err) + + testStore.gc(time.Now()) + val, err := testStore.Get("john") + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, testVal, val) + +} + +func Test_MSSQL_Non_UTF8(t *testing.T) { + val := []byte("0xF5") + + err := testStore.Set("0xF6", val, 0) + utils.AssertEqual(t, nil, err) + + result, err := testStore.Get("0xF6") + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, val, result) +} + +func Test_SslRequiredMode(t *testing.T) { + defer func() { + if recover() == nil { + utils.AssertEqual(t, true, nil, "Connection was established with a `require`") + } + }() + _ = New(Config{ + Database: "fiber", + Username: "username", + Password: "password", + Reset: true, + SslMode: "require", + }) +} + +func Test_MSSQL_Close(t *testing.T) { + utils.AssertEqual(t, nil, testStore.Close()) +} + +func Test_MSSQL_Conn(t *testing.T) { + utils.AssertEqual(t, true, testStore.Conn() != nil) +}