Add support to MSSQL

This commit is contained in:
gandaldf
2022-10-25 17:08:35 +02:00
parent 66d62ec7cb
commit 1f2c29fd9a
8 changed files with 803 additions and 0 deletions

5
go.mod
View File

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

51
go.sum
View File

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

135
mssql/README.md Normal file
View File

@@ -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/<user>/<repo>
```
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",
}
```

146
mssql/config.go Normal file
View File

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

8
mssql/go.mod Normal file
View File

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

40
mssql/go.sum Normal file
View File

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

235
mssql/mssql.go Normal file
View File

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

183
mssql/mssql_test.go Normal file
View File

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