-
Notifications
You must be signed in to change notification settings - Fork 58
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #33 from schigh/master
Add SQL Checker
- Loading branch information
Showing
8 changed files
with
1,070 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,3 @@ | ||
*.sketch | ||
/vendor/ | ||
.DS_Store |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
package checkers | ||
|
||
import ( | ||
"context" | ||
"database/sql" | ||
"fmt" | ||
) | ||
|
||
//go:generate counterfeiter -o ../fakes/isqlpinger.go . SQLPinger | ||
//go:generate counterfeiter -o ../fakes/isqlqueryer.go . SQLQueryer | ||
//go:generate counterfeiter -o ../fakes/isqlexecer.go . SQLExecer | ||
|
||
// SQLPinger is an interface that allows direct pinging of the database | ||
type SQLPinger interface { | ||
PingContext(ctx context.Context) error | ||
} | ||
|
||
// SQLQueryer is an interface that allows querying of the database | ||
type SQLQueryer interface { | ||
QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) | ||
} | ||
|
||
// SQLExecer is an interface that allows executing of queries in the database | ||
type SQLExecer interface { | ||
ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) | ||
} | ||
|
||
// SQLQueryerResultHandler is the BYO function to | ||
// handle the result of an SQL SELECT query | ||
type SQLQueryerResultHandler func(rows *sql.Rows) (bool, error) | ||
|
||
// SQLExecerResultHandler is the BYO function | ||
// to handle a database exec result | ||
type SQLExecerResultHandler func(result sql.Result) (bool, error) | ||
|
||
// SQLConfig is used for configuring a database check. | ||
// One of the Pinger, Queryer, or Execer fields is required. | ||
// | ||
// | ||
// If Execer is set, it will take precedence over Queryer and Pinger, | ||
// Execer implements the SQLExecer interface in this package. | ||
// The sql.DB and sql.TX structs both implement this interface. | ||
// | ||
// Note that if the Execer is set, then the ExecerResultHandler | ||
// and Query values MUST also be set | ||
// | ||
// | ||
// If Queryer is set, it will take precedence over Pinger. | ||
// SQLQueryer implements the SQLQueryer interface in this package. | ||
// The sql.DB and sql.TX structs both implement this interface. | ||
// | ||
// Note that if the Queryer is set, then the QueryerResultHandler | ||
// and Query values MUST also be set | ||
// | ||
// | ||
// Pinger implements the SQLPinger interface in this package. | ||
// The sql.DB struct implements this interface. | ||
type SQLConfig struct { | ||
// Pinger is the value implementing SQLPinger | ||
Pinger SQLPinger | ||
|
||
// Queryer is the value implementing SQLQueryer | ||
Queryer SQLQueryer | ||
|
||
// Execer is the value implementing SQLExecer | ||
Execer SQLExecer | ||
|
||
// Query is the parameterized SQL query required | ||
// with both Queryer and Execer | ||
Query string | ||
|
||
// Params are the SQL query parameters, if any | ||
Params []interface{} | ||
|
||
// QueryerResultHandler handles the result of | ||
// the QueryContext function | ||
QueryerResultHandler SQLQueryerResultHandler | ||
|
||
// ExecerResultHandler handles the result of | ||
// the ExecContext function | ||
ExecerResultHandler SQLExecerResultHandler | ||
} | ||
|
||
// SQL implements the "ICheckable" interface | ||
type SQL struct { | ||
Config *SQLConfig | ||
} | ||
|
||
// NewSQL creates a new database checker that can be used for ".AddCheck(s)". | ||
func NewSQL(cfg *SQLConfig) (*SQL, error) { | ||
if err := validateSQLConfig(cfg); err != nil { | ||
return nil, err | ||
} | ||
|
||
return &SQL{ | ||
Config: cfg, | ||
}, nil | ||
} | ||
|
||
// DefaultQueryHandler is the default SQLQueryer result handler | ||
// that assumes one row was returned from the passed query | ||
func DefaultQueryHandler(rows *sql.Rows) (bool, error) { | ||
defer rows.Close() | ||
|
||
numRows := 0 | ||
for rows.Next() { | ||
numRows++ | ||
} | ||
|
||
return numRows == 1, nil | ||
} | ||
|
||
// DefaultExecHandler is the default SQLExecer result handler | ||
// that assumes one row was affected in the passed query | ||
func DefaultExecHandler(result sql.Result) (bool, error) { | ||
affectedRows, err := result.RowsAffected() | ||
if err != nil { | ||
return false, err | ||
} | ||
|
||
return affectedRows == int64(1), nil | ||
} | ||
|
||
// this makes sure the sql check is properly configured | ||
func validateSQLConfig(cfg *SQLConfig) error { | ||
if cfg == nil { | ||
return fmt.Errorf("config is required") | ||
} | ||
|
||
if cfg.Execer == nil && cfg.Queryer == nil && cfg.Pinger == nil { | ||
return fmt.Errorf("one of Execer, Queryer, or Pinger is required in SQLConfig") | ||
} | ||
|
||
if (cfg.Execer != nil || cfg.Queryer != nil) && len(cfg.Query) == 0 { | ||
return fmt.Errorf("SQLConfig.Query is required") | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// Status is used for performing a database ping against a dependency; it satisfies | ||
// the "ICheckable" interface. | ||
func (s *SQL) Status() (interface{}, error) { | ||
if err := validateSQLConfig(s.Config); err != nil { | ||
return nil, err | ||
} | ||
|
||
switch { | ||
// check for SQLExecer first | ||
case s.Config.Execer != nil: | ||
// if the result handler is nil, use the default | ||
if s.Config.ExecerResultHandler == nil { | ||
s.Config.ExecerResultHandler = DefaultExecHandler | ||
} | ||
// run the execer | ||
return s.runExecer() | ||
// check for SQLQueryer next | ||
case s.Config.Queryer != nil: | ||
// if the result handler is nil, use the default | ||
if s.Config.QueryerResultHandler == nil { | ||
s.Config.QueryerResultHandler = DefaultQueryHandler | ||
} | ||
// run the queryer | ||
return s.runQueryer() | ||
// finally, must be a pinger | ||
default: | ||
ctx := context.Background() | ||
return nil, s.Config.Pinger.PingContext(ctx) | ||
} | ||
} | ||
|
||
// This will run the execer from the Status func | ||
func (s *SQL) runExecer() (interface{}, error) { | ||
ctx := context.Background() | ||
result, err := s.Config.Execer.ExecContext(ctx, s.Config.Query, s.Config.Params...) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
ok, err := s.Config.ExecerResultHandler(result) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if !ok { | ||
return nil, fmt.Errorf("userland exec result handler returned false") | ||
} | ||
|
||
return nil, nil | ||
} | ||
|
||
// This will run the queryer from the Status func | ||
func (s *SQL) runQueryer() (interface{}, error) { | ||
ctx := context.Background() | ||
rows, err := s.Config.Queryer.QueryContext(ctx, s.Config.Query, s.Config.Params...) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// the BYO result handler is responsible for closing the rows | ||
|
||
ok, err := s.Config.QueryerResultHandler(rows) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if !ok { | ||
return nil, fmt.Errorf("userland query result handler returned false") | ||
} | ||
|
||
return nil, nil | ||
} |
Oops, something went wrong.