diff --git a/.chglog/CHANGELOG.tpl.md b/.chglog/CHANGELOG.tpl.md new file mode 100755 index 0000000..9df5623 --- /dev/null +++ b/.chglog/CHANGELOG.tpl.md @@ -0,0 +1,67 @@ +# CHANGELOG + +{{ if .Versions -}} + +## [Unreleased] +{{ if .Unreleased.CommitGroups -}} +{{ range .Unreleased.CommitGroups }} +### {{ .Title }} +{{ range .Commits -}} +{{ if not (contains .Subject "") -}} +- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ if .Subject }}{{ .Subject }}{{ else }}{{ .Header }}{{ end }} +{{ end -}} +{{ end -}} +{{ end }} +{{ else }} +{{ range .Unreleased.Commits -}} +- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ if .Subject }}{{ .Subject }}{{ else }}{{ .Header }}{{ end }} +{{ end }} +{{ end -}} + +{{- if .Unreleased.NoteGroups -}} +{{ range .Unreleased.NoteGroups -}} +### {{ .Title }} +{{ range .Notes }} +{{ .Body }} +{{ end }} +{{ end -}} +{{ end -}} +{{ end -}} + +{{ range .Versions }} + +## {{ if .Tag.Previous }}[{{ .Tag.Name }}]{{ else }}{{ .Tag.Name }}{{ end }} - {{ datetime "2006-01-02" .Tag.Date }} +{{ if .CommitGroups -}} +{{ range .CommitGroups }} +### {{ .Title }} +{{ range .Commits -}} +{{ if not (contains .Subject "") -}} +- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ if .Subject }}{{ .Subject }}{{ else }}{{ .Header }}{{ end }} +{{ end -}} +{{ end -}} +{{ end }} +{{ else }} +{{ range .Commits -}} +- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ if .Subject }}{{ .Subject }}{{ else }}{{ .Header }}{{ end }} +{{ end }} +{{ end -}} + +{{- if .NoteGroups -}} +{{ range .NoteGroups -}} +### {{ .Title }} +{{ range .Notes }} +{{ .Body }} +{{ end }} +{{ end -}} +{{ end -}} +{{ end -}} + +{{- if .Versions }} + +[Unreleased]: {{ .Info.RepositoryURL }}/compare/{{ $latest := index .Versions 0 }}{{ $latest.Tag.Name }}...HEAD +{{ range .Versions -}} +{{ if .Tag.Previous -}} +[{{ .Tag.Name }}]: {{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }} +{{ end -}} +{{ end -}} +{{ end -}} diff --git a/.chglog/config.yml b/.chglog/config.yml new file mode 100755 index 0000000..4feec6f --- /dev/null +++ b/.chglog/config.yml @@ -0,0 +1,75 @@ +style: github +template: CHANGELOG.tpl.md +info: + title: CHANGELOG + repository_url: https://github.com/arsham/dbtools + +options: + commits: + # filters: + # Type: + # - feat + # - add + # - return + # - feature + # - fix + # - perf + # - refactor + + commit_groups: + # group_by: Type + + title_maps: + feat: Added + Feat: Added + add: Added + Add: Added + feature: New Feature + Feature: New Feature + fix: Fixed + Fix: Fixed + merge: Merged + Merge: Merged + perf: Performance Improvements + Perf: Performance Improvements + refactor: Code Refactoring + Refactor: Code Refactoring + Increase: Code Refactoring + + header: + # { header } + # {type}{scope} { subject } + # add(core): new feature to the thing (closes #666) + # We are ignoring paranteses around the scope. + pattern: "^((\\w+)(\\((\\w+)\\))?:?\\s(.+))$" + pattern_maps: + - Subject + - Type + - + - Scope + + issues: + prefix: + - # + + refs: + actions: + - Closes + - Fixes + - Refs + + merges: + pattern: "^Merge branch '(\\w+)'$" + pattern_maps: + - Source + + reverts: + pattern: "^Revert \"([\\s\\S]*)\"$" + pattern_maps: + - Header + + notes: + keywords: + - BREAKING CHANGE + - IMPORTANT + - important diff --git a/.gitignore b/.gitignore index 87b0c13..c67a417 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out vendor/ +tmp/ diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..de83c10 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,99 @@ +linters-settings: + govet: + check-shadowing: true + settings: + printf: + funcs: + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf + golint: + min-confidence: 0 + gocyclo: + min-complexity: 15 + maligned: + suggest-new: true + goconst: + min-len: 2 + min-occurrences: 2 + misspell: + locale: UK + lll: + line-length: 140 + goimports: + local-prefixes: github.com/golangci/golangci-lint + gocritic: + enabled-tags: + - diagnostic + - experimental + - opinionated + - performance + - style + disabled-checks: + - wrapperFunc + - dupImport # https://github.com/go-critic/go-critic/issues/845 + funlen: + lines: 100 + statements: 50 + + +issues: + # Excluding configuration per-path, per-linter, per-text and per-source + exclude-rules: + - path: _test\.go + linters: + - gosec # security check is not impoerant in tests + - dupl # we usualy duplicate code in tests + +run: + skip-dirs: + - model + - tmp + - bin + - scripts + + tests: true + build-tags: + - integration + +linters: + disable-all: true + fast: true + enable: + - asciicheck + - bodyclose + - dogsled + - depguard + - dupl + - gocognit + - goconst + - gocritic + - gocyclo + - godot + - godox + - golint + - goprintffuncname + - gosec + - gosimple + - interfacer + - maligned + - misspell + - nakedret + - nestif + - prealloc + - rowserrcheck + - scopelint + - staticcheck + - stylecheck + - unconvert + - unparam + - unused + - whitespace + +# golangci.com configuration +# https://github.com/golangci/golangci/wiki/Configuration +service: + golangci-lint-version: 1.30.x + prepare: + - echo "here I can run custom commands, but no preparation needed for this repo" diff --git a/.travis.yml b/.travis.yml index a4fbe3e..eb8e22c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: go -dist: xenial + +dist: focal os: - linux @@ -8,12 +9,24 @@ env: - GO111MODULE=on go: - - 1.11.x - - 1.12.x - - 1.13.x + - 1.14.x + - 1.15.x + - tip + +matrix: + allow_failures: + - go: tip + +before_install: + - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.30.0 script: - - go test -failfast -v -coverprofile=coverage.txt -covermode=atomic ./... + - make ci_tests after_success: - bash <(curl -s https://codecov.io/bash) + +cache: + directories: + - $HOME/.cache/go-build + - $HOME/gopath/pkg/mod diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f11a9e0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,59 @@ +# CHANGELOG + + +## [Unreleased] + + + + +## [v0.4.0] - 2020-08-16 + +### Added +- Add support for pgxpool + + + +## [v0.3.2] - 2019-10-16 + +### Handle +- Handle function panics + + + +## [v0.3.1] - 2019-07-17 + +### Update +- Update badges + + + +## [v0.3.0] - 2019-07-17 + +### Added +- Add db helpers and rename the repo to dbtools + + + +## [v0.2.0] - 2019-07-16 + +### Added +- Add mocha report and update README.md file + + + +## v0.1.0 - 2019-07-02 + +### Added +- Add ValueRecorder and OkValue + +### Initial +- Initial commit + + + +[Unreleased]: https://github.com/arsham/dbtools/compare/v0.4.0...HEAD +[v0.4.0]: https://github.com/arsham/dbtools/compare/v0.3.2...v0.4.0 +[v0.3.2]: https://github.com/arsham/dbtools/compare/v0.3.1...v0.3.2 +[v0.3.1]: https://github.com/arsham/dbtools/compare/v0.3.0...v0.3.1 +[v0.3.0]: https://github.com/arsham/dbtools/compare/v0.2.0...v0.3.0 +[v0.2.0]: https://github.com/arsham/dbtools/compare/v0.1.0...v0.2.0 diff --git a/Makefile b/Makefile index e5dd727..845e44c 100644 --- a/Makefile +++ b/Makefile @@ -1,19 +1,63 @@ -.PHONY: test -test: - @echo "running tests on $(run). waiting for changes..." - @-zsh -c "go test ./...; repeat 100 printf '#'; echo" - @reflex -d none -r "(\.go$$)|(go.mod)" -- zsh -c "go test ./...; repeat 100 printf '#'" +help: ## Show help messages. + @grep -E '^[0-9a-zA-Z_-]+:(.*?## .*)?$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +run="." +dir="./..." +short="-short" +flags="" +timeout=40s -.PHONY: test_race -test_race: + +.PHONY: tests +tests: ## Run unit tests in watch mode. You can set: [run, timeout, short, dir, flags]. Example: make tests flags="-race". @echo "running tests on $(run). waiting for changes..." - @-zsh -c "go test -race ./...; repeat 100 printf '#'; echo" - @reflex -d none -r "(\.go$$)|(go.mod)" -- zsh -c "go test -race ./...; repeat 100 printf '#'" + @-zsh -c "go test -trimpath --timeout=$(timeout) $(short) $(dir) -run $(run) $(flags); repeat 100 printf '#'; echo" + @reflex -d none -r "(\.go$$)|(go.mod)" -- zsh -c "go test -trimpath --timeout=$(timeout) $(short) $(dir) -run $(run) $(flags); repeat 100 printf '#'" + -.PHONY: third-party -third-party: +.PHONY: dependencies +dependencies: ## Install dependencies requried for development operations. @go get -u github.com/cespare/reflex + @go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.30.0 + @go get -u github.com/git-chglog/git-chglog/cmd/git-chglog + @go get github.com/stretchr/testify/mock + @go get github.com/vektra/mockery/.../ + @go mod tidy + + +.PHONY: ci_tests +ci_tests: ## Run tests for CI. + go fmt ./... + go vet ./... + golangci-lint run ./... + go test -trimpath --timeout=10m -failfast -v -race -covermode=atomic -coverprofile=coverage.out ./... + + +.PHONY: changelog +changelog: ## Update the changelog. + @git-chglog > CHANGELOG.md + @echo "Changelog has been updated." + + +.PHONY: changelog_release +changelog_release: ## Update the changelog with a release tag. + @git-chglog --next-tag $(tag) > CHANGELOG.md + @echo "Changelog has been updated." + .PHONY: clean -clean: - go clean -cache -testcache +clean: ## Clean test caches and tidy up modules. + @go clean -testcache + @go mod tidy + + +.PHONY: mocks +mocks: ## Generate mocks in all packages. + @go generate ./... + + +.PHONY: coverage +coverage: ## Show the test coverage on browser. + go test -covermode=count -coverprofile=coverage.out ./... + go tool cover -func=coverage.out | tail -n 1 + go tool cover -html=coverage.out diff --git a/README.md b/README.md index 9a35378..576c11f 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,34 @@ # dbtools -[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) -[![GoDoc](https://godoc.org/github.com/arsham/dbtools?status.svg)](http://godoc.org/github.com/arsham/dbtools) +[![PkgGoDev](https://pkg.go.dev/badge/github.com/arsham/dbtools)](https://pkg.go.dev/github.com/arsham/dbtools) +![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/arsham/dbtools) [![Build Status](https://travis-ci.org/arsham/dbtools.svg?branch=master)](https://travis-ci.org/arsham/dbtools) [![Coverage Status](https://codecov.io/gh/arsham/dbtools/branch/master/graph/badge.svg)](https://codecov.io/gh/arsham/dbtools) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) -This library has a few helpers for using in production code and -[go-sqlmock][go-sqlmock] tests. There is also a `Mocha` inspired reporter for -[spec BDD library][spec]. +This library contains goroutine safe helpers for retrying transactions until +they succeed and handles errors in a developer friendly way. There are helpers +for using with [go-sqlmock][go-sqlmock] in tests. There is also a `Mocha` +inspired reporter for [spec BDD library][spec]. -This library supports Go 1.11 and above. +This library supports `Go >= 1.14`. 1. [Transaction](#transaction) - * [WithTransaction](#withtransaction) - * [Retry](#retry) - * [RetryTransaction](#retrytransaction) -2. [Spec Reports](#spec-reports) - * [Usage](#usage) -3. [SQLMock Helpers](#sqlmock-helpers) + * [PGX Pool](#pgx-pool) + * [Standard Library](#standard-library) + * [Deprecation Notice](#deprecation-notice) +2. [SQLMock Helpers](#sqlmock-helpers) * [ValueRecorder](#valuerecorder) * [OkValue](#okvalue) -4. [Testing](#testing) +3. [Spec Reports](#spec-reports) + * [Usage](#usage) +4. [Development](#development) 5. [License](#license) ## Transaction -### WithTransaction - -`WithTransaction` helps you reduce the amount of code you put in the logic by -taking care of errors. For example instead of writing: +`Transaction` helps you reduce the amount of code you put in the logic by taking +care of errors. For example instead of writing: ```go tx, err := db.Begin() @@ -55,62 +55,79 @@ return errors.Wrap(tx.Commit(), "committing transaction") ``` -You can write: +You will write: + ```go -return dbtools.WithTransaction(db, firstQueryCall, secondQueryCall, thirdQueryCall) +// for using with pgx connections: +tr, err := dbtools.NewTransaction(conn) +// handle error, and reuse tr +return tr.PGX(ctx, firstQueryCall, secondQueryCall, thirdQueryCall) + +// or to use with stdlib sql.DB: +tr, err := dbtools.NewTransaction(conn) +// handle error, and reuse tr +return tr.DB(ctx, firstQueryCall, secondQueryCall, thirdQueryCall) ``` -Function types should be of `func(*sql.Tx) error`. - -### Retry +At any point a transaction function returns an error, the whole transaction is +started over. -`Retry` calls your function, and if it errors it calls it again with a delay. -Every time the function returns an error it increases the delay. Eventually it -returns the last error or nil if one call is successful. +You may set the retry count, delays, and the delay method by passing +`dbtools.ConfigFunc` functions to the constructor. If you don't pass any +config, `PGX` and `DB` methods will run only once. -You can use this function in non-database situations too. +You can prematurely stop retrying by returning a `retry.StopError` error: ```go -dbtools.Retry(10, time.Second. func(i int) error { - logger.Debugf("%d iteration", i) - return myFunctionCall() +err = tr.PGX(ctx, func(tx pgx.Tx) error { + _, err := tx.Exec(ctx, query) + return retry.StopError{Err: err} }) ``` -### RetryTransaction +See [retry][retry] library for more information. -`RetryTransaction` is a combination of `WithTransaction` and `Retry`. It stops -the retry if the context is cancelled/done. +### PGX Pool + +Your transaction functions should be of `func(pgx.Tx) error` type. To try up to +20 time until your queries succeed: ```go -err := dbtools.RetryTransaction(ctx, db, 10, time.Millisecond * 10, - firstQueryCall, - secondQueryCall, - thirdQueryCall, -) -// error check +// conn is a *sql.DB instance +tr, err := dbtools.NewTransaction(conn, dbtools.Retry(20)) +// handle error +err = tr.PGX(ctx, func(tx pgx.Tx) error { + // use tx to run your queries + return err +}, func(tx pgx.Tx) error { + return err +}) +// handle error ``` -## Spec Reports - -`Mocha` is a reporter for printing Mocha inspired reports when using -[spec BDD library][spec]. +### Standard Library -### Usage +Your transaction functions should be of `func(dbtools.Tx) error` type. To try up to +20 time until your queries succeed: ```go -import "github.com/arsham/dbtools/dbtesting" - -func TestFoo(t *testing.T) { - spec.Run(t, "Foo", func(t *testing.T, when spec.G, it spec.S) { - // ... - }, spec.Report(&dbtesting.Mocha{})) -} - +// conn is a *pgxpool.Pool instance +tr, err := dbtools.NewTransaction(conn, dbtools.Retry(20)) +// handle error +err = tr.DB(ctx, func(tx dbtools.Tx) error { + // use tx to run your queries + return err +}, func(tx dbtools.Tx) error { + return err +}) +// handle error ``` -You can set an `io.Writer` to `Mocha.Out` to redirect the output, otherwise it -prints to the `os.Stdout`. +### Deprecation Notice + +`WithTransaction` and `RetryTransaction` functions are deprecated. Please use +`Transaction` instead. `Retry` function is also deprecated in favour of the +[retry][retry] library. ## SQLMock Helpers @@ -201,21 +218,57 @@ mock.ExpectExec("INSERT INTO life .+"). ) ``` -## Testing +## Spec Reports + +`Mocha` is a reporter for printing Mocha inspired reports when using +[spec BDD library][spec]. + +### Usage + +```go +import "github.com/arsham/dbtools/dbtesting" + +func TestFoo(t *testing.T) { + spec.Run(t, "Foo", func(t *testing.T, when spec.G, it spec.S) { + // ... + }, spec.Report(&dbtesting.Mocha{})) +} + +``` + +You can set an `io.Writer` to `Mocha.Out` to redirect the output, otherwise it +prints to the `os.Stdout`. + + +## Development + +Run the `tests` target for watching file changes and running tests: + +```bash +make tests +``` + +You can pass flags as such: + + +```bash +make tests flags="-race -v -count=5" +``` -To run the tests: +You need to run the `dependencies` target for installing [reflex][reflex] task +runner: ```bash -make +make dependencies ``` -`test_race` target runs tests with `-race` flag. `third-party` installs -[reflex][reflex] task runner. ## License Use of this source code is governed by the Apache 2.0 license. License can be found in the [LICENSE](./LICENSE) file. +[retry]: https://github.com/arsham/retry +[pgx]: https://github.com/jackc/pgx [go-sqlmock]: https://github.com/DATA-DOG/go-sqlmock [spec]: https://github.com/sclevine/spec [reflex]: https://github.com/cespare/reflex diff --git a/contract.go b/contract.go new file mode 100644 index 0000000..0be1f2a --- /dev/null +++ b/contract.go @@ -0,0 +1,95 @@ +package dbtools + +import ( + "context" + "database/sql" + "errors" + "time" + + "github.com/arsham/retry" + "github.com/jackc/pgx/v4" +) + +var ( + // ErrEmptyDatabase is returned when no database connection is set. + ErrEmptyDatabase = errors.New("no database connection is set") + + errPanic = errors.New("function caused a panic") +) + +// DB is the contract for beginning a transaction with a *sql.DB object. +//go:generate mockery -name DB -filename db_mock.go +type DB interface { + Begin() (Tx, error) + BeginTx(ctx context.Context, opts *sql.TxOptions) (Tx, error) +} + +// Pool is the contract for beginning a transaction with a pgxpool db +// connection. +//go:generate mockery -name Pool -filename pool_mock.go +type Pool interface { + Begin(ctx context.Context) (pgx.Tx, error) + BeginTx(ctx context.Context, txOptions pgx.TxOptions) (pgx.Tx, error) +} + +//nolint:unused // only used for mocking. +//go:generate mockery -name pgxTx -filename pgx_tx_mock.go -structname PGXTx +type pgxTx interface { + pgx.Tx +} + +// Tx is a transaction began with sql.DB. +//go:generate mockery -name Tx -filename tx_mock.go +type Tx interface { + Commit() error + Exec(query string, args ...interface{}) (sql.Result, error) + ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) + Prepare(query string) (*sql.Stmt, error) + PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) + Query(query string, args ...interface{}) (*sql.Rows, error) + QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) + QueryRow(query string, args ...interface{}) *sql.Row + QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row + Rollback() error + Stmt(stmt *sql.Stmt) *sql.Stmt + StmtContext(ctx context.Context, stmt *sql.Stmt) *sql.Stmt +} + +// A ConfigFunc function sets up a Transaction. +type ConfigFunc func(*Transaction) + +// RetryCount defines a transaction should be tried n times. If n is 0, it will +// be set as 1. +func RetryCount(n int) ConfigFunc { + return func(t *Transaction) { + t.retries = n + } +} + +// RetryDelay is the amount of delay between each unsuccessful tries. Set +// DelayMethod for the method of delay duration. +func RetryDelay(d time.Duration) ConfigFunc { + return func(t *Transaction) { + t.delay = d + } +} + +// DelayMethod decides how to delay between each tries. Default is +// retry.StandardDelay. +func DelayMethod(m retry.DelayMethod) ConfigFunc { + return func(t *Transaction) { + t.method = m + } +} + +type dbWrapper struct { + db *sql.DB +} + +func (d *dbWrapper) Begin() (Tx, error) { + return d.db.Begin() +} + +func (d *dbWrapper) BeginTx(ctx context.Context, opts *sql.TxOptions) (Tx, error) { + return d.db.BeginTx(ctx, opts) +} diff --git a/db.go b/db.go index 0fa5b84..f4c0c33 100644 --- a/db.go +++ b/db.go @@ -6,15 +6,162 @@ import ( "io" "time" + "github.com/arsham/retry" "github.com/hashicorp/go-multierror" + "github.com/jackc/pgx/v4" "github.com/pkg/errors" ) -var errPanic = errors.New("function caused a panic") +// Transaction is a concurrent-safe object that can retry a transaction on +// either a sql.DB or a pgxpool connection until it succeeds. +// +// DB and PGX will try transaction functions one-by-one until all of them return +// nil, then commits the transaction. If any of the transactions return any +// error other than retry.StopError, it will retry the transaction until the +// retry count is exhausted. If a running function returns a retry.StopError, +// the transaction will be rolled-back and would stop retrying. Tryouts will be +// stopped when the passed contexts are cancelled. +// +// If all attempts return errors, the last error is returned. If a +// retry.StopError is returned, transaction is rolled back and the Err inside +// the retry.StopError is returned. There will be delays between tries defined +// by the retry.DelayMethod and Delay duration. +// +// Any panic in transactions will be wrapped in an error and will be counted as +// an error, either being retried or returned. +// +// It's an error to invoke the methods without their respective connections are +// set. +type Transaction struct { + loop retry.Retry + delay time.Duration + retries int + method retry.DelayMethod + db DB + pool Pool +} + +// NewTransaction returns an error if conn is not a DB, Pool, or *sql.DB +// connection. +func NewTransaction(conn interface{}, conf ...ConfigFunc) (*Transaction, error) { + if conn == nil { + return nil, ErrEmptyDatabase + } + t := &Transaction{} + switch db := conn.(type) { + case DB: + t.db = db + case Pool: + t.pool = db + case *sql.DB: + t.db = &dbWrapper{db: db} + default: + return nil, ErrEmptyDatabase + } + + for _, fn := range conf { + fn(t) + } + if t.db == nil && t.pool == nil { + return nil, ErrEmptyDatabase + } + if t.retries < 1 { + t.retries = 1 + } + t.loop = retry.Retry{ + Attempts: t.retries, + Delay: t.delay, + Method: t.method, + } + return t, nil +} + +// PGX returns an error if a pgxpool connection is not set. +func (t *Transaction) PGX(ctx context.Context, transactions ...func(pgx.Tx) error) error { + if t.pool == nil { + return ErrEmptyDatabase + } + return t.loop.Do(func() error { + tx, err := t.pool.Begin(ctx) + if err != nil { + return errors.Wrap(err, "starting transaction") + } + for _, fn := range transactions { + select { + case <-ctx.Done(): + e := errors.Wrap(tx.Rollback(ctx), "rolling back transaction") + return retry.StopError{ + Err: multierror.Append(ctx.Err(), e).ErrorOrNil(), + } + default: + } + var err error + func() { + defer func() { + if r := recover(); r != nil { + err = errors.Wrapf(errPanic, "%v", r) + } + }() + err = fn(tx) + }() + if err != nil { + e := errors.Wrap(tx.Rollback(ctx), "rolling back transaction") + e = multierror.Append(err, e).ErrorOrNil() + if _, ok := err.(retry.StopError); ok { + e = retry.StopError{Err: e} + } + return e + } + } + return errors.Wrap(tx.Commit(ctx), "committing transaction") + }) +} + +// DB returns an error if a sql.DB connection is not set. +func (t *Transaction) DB(ctx context.Context, transactions ...func(Tx) error) error { + if t.db == nil { + return ErrEmptyDatabase + } + return t.loop.Do(func() error { + tx, err := t.db.BeginTx(ctx, nil) + if err != nil { + return errors.Wrap(err, "starting transaction") + } + for _, fn := range transactions { + select { + case <-ctx.Done(): + e := errors.Wrap(tx.Rollback(), "rolling back transaction") + return retry.StopError{ + Err: multierror.Append(ctx.Err(), e).ErrorOrNil(), + } + default: + } + var err error + func() { + defer func() { + if r := recover(); r != nil { + err = errors.Wrapf(errPanic, "%v", r) + } + }() + err = fn(tx) + }() + if err != nil { + e := errors.Wrap(tx.Rollback(), "rolling back transaction") + e = multierror.Append(err, e).ErrorOrNil() + if _, ok := err.(retry.StopError); ok { + e = retry.StopError{Err: e} + } + return e + } + } + return errors.Wrap(tx.Commit(), "committing transaction") + }) +} // WithTransaction creates a transaction on db and uses it to call fn functions // one by one. The first function that returns an error or panics will cause the // loop to stop and transaction to be rolled back. +// Deprecated: use Transaction instead. func WithTransaction(db *sql.DB, fn ...func(*sql.Tx) error) error { tx, err := db.Begin() if err != nil { @@ -41,6 +188,7 @@ func WithTransaction(db *sql.DB, fn ...func(*sql.Tx) error) error { // Retry calls fn for retries times until it returns nil. If retries is zero fn // would not be called. It delays and retries if the function returns any errors // or panics. The fn function receives the current iteration as its argument. +// Deprecated: use http://github.com/arsham/retry library instead. func Retry(retries int, delay time.Duration, fn func(int) error) error { var err error for i := 0; i < retries; i++ { @@ -63,6 +211,7 @@ func Retry(retries int, delay time.Duration, fn func(int) error) error { // RetryTransaction combines WithTransaction and Retry calls. It stops the call // if context is times out or cancelled. +// Deprecated: use Transaction instead. func RetryTransaction(ctx context.Context, db *sql.DB, retries int, delay time.Duration, fn ...func(*sql.Tx) error) error { select { case <-ctx.Done(): diff --git a/db_example_test.go b/db_example_test.go new file mode 100644 index 0000000..442be04 --- /dev/null +++ b/db_example_test.go @@ -0,0 +1,130 @@ +package dbtools_test + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/arsham/dbtools" + "github.com/arsham/retry" + "github.com/jackc/pgx/v4" + "github.com/stretchr/testify/assert" +) + +func ExampleNewTransaction() { + // This setup tries the transaction only once. + dbtools.NewTransaction(&exampleConn{}) + + // This setup tries 100 times until succeeds. The delay is set to 10ms and + // it uses the retry.IncrementalDelay method, which means every time it + // increments the delay between retries with a jitter to avoid thunder herd + // problem. + dbtools.NewTransaction(&exampleConn{}, + dbtools.RetryCount(100), + dbtools.RetryDelay(10*time.Millisecond), + dbtools.DelayMethod(retry.IncrementalDelay), + ) +} + +func ExampleTransaction_PGX() { + tr, err := dbtools.NewTransaction(&exampleConn{}) + if err != nil { + panic(err) + } + err = tr.PGX(context.Background(), func(pgx.Tx) error { + fmt.Println("Running first query.") + return nil + }, func(pgx.Tx) error { + fmt.Println("Running second query.") + return nil + }) + fmt.Printf("Transaction's error: %v", err) + + // Output: + // Running first query. + // Running second query. + // Transaction's error: +} + +func ExampleTransaction_PGX_retries() { + tr, err := dbtools.NewTransaction(&exampleConn{}, dbtools.RetryCount(10)) + if err != nil { + panic(err) + } + called := false + err = tr.PGX(context.Background(), func(pgx.Tx) error { + fmt.Println("Running first query.") + return nil + }, func(pgx.Tx) error { + if !called { + called = true + fmt.Println("Second query error.") + return assert.AnError + } + fmt.Println("Running second query.") + return nil + }) + fmt.Printf("Transaction's error: %v", err) + + // Output: + // Running first query. + // Second query error. + // Running first query. + // Running second query. + // Transaction's error: +} + +func ExampleTransaction_PGX_stopTrying() { + // This example shows how to stop trying when we know an error is not + // recoverable. + tr, err := dbtools.NewTransaction(&exampleConn{}, + dbtools.RetryCount(100), + dbtools.RetryDelay(time.Second), + ) + if err != nil { + panic(err) + } + err = tr.PGX(context.Background(), func(pgx.Tx) error { + fmt.Println("Running first query.") + return nil + }, func(pgx.Tx) error { + fmt.Println("Running second query.") + return retry.StopError{Err: assert.AnError} + }) + fmt.Printf("Transaction returns my error: %t", strings.Contains(err.Error(), assert.AnError.Error())) + + // Output: + // Running first query. + // Running second query. + // Transaction returns my error: true +} + +func ExampleTransaction_PGX_panics() { + tr, err := dbtools.NewTransaction(&exampleConn{}, dbtools.RetryCount(10)) + if err != nil { + panic(err) + } + calls := 0 + err = tr.PGX(context.Background(), func(pgx.Tx) error { + calls++ + fmt.Printf("Call #%d.\n", calls) + if calls < 5 { + panic("We have a panic!") + } + fmt.Println("All done.") + return nil + }) + fmt.Printf("Transaction's error: %v\n", err) + fmt.Printf("Called %d times.\n", calls) + + // Output: + // Call #1. + // Call #2. + // Call #3. + // Call #4. + // Call #5. + // All done. + // Transaction's error: + // Called 5 times. +} diff --git a/db_test.go b/db_test.go index f275cf6..e3844ac 100644 --- a/db_test.go +++ b/db_test.go @@ -9,12 +9,836 @@ import ( "github.com/DATA-DOG/go-sqlmock" "github.com/arsham/dbtools" + "github.com/arsham/dbtools/mocks" + "github.com/arsham/retry" "github.com/hashicorp/go-multierror" + "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v4/pgxpool" + "github.com/jackc/pgx/v4/stdlib" "github.com/pkg/errors" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) +func TestNewTransaction(t *testing.T) { + t.Parallel() + db, _, err := sqlmock.New() + require.NoError(t, err) + defer db.Close() + tcs := map[string]struct { + db interface{} + conf []dbtools.ConfigFunc + wantErr error + }{ + "nil db": {nil, nil, dbtools.ErrEmptyDatabase}, + "nil pgxpool": {nil, nil, dbtools.ErrEmptyDatabase}, + "nil sql.DB": {nil, nil, dbtools.ErrEmptyDatabase}, + "wrong db type": {"db", nil, dbtools.ErrEmptyDatabase}, + "db": {&mocks.DB{}, nil, nil}, + "pool": {&mocks.Pool{}, nil, nil}, + "sql.DB": {db, nil, nil}, + "low attempts": {db, []dbtools.ConfigFunc{dbtools.RetryCount(-1)}, nil}, + } + for name, tc := range tcs { + tc := tc + t.Run(name, func(t *testing.T) { + _, err := dbtools.NewTransaction(tc.db, tc.conf...) + if tc.wantErr == nil { + assert.NoError(t, err) + return + } + assertInError(t, err, tc.wantErr) + }) + } +} + +func TestTransaction(t *testing.T) { + t.Run("PGX", testTransactionPGX) + t.Run("DB", testTransactionDB) +} + +func testTransactionPGX(t *testing.T) { + t.Run("NilDatabase", testTransactionPGXNilDatabase) + t.Run("BeingError", testTransactionPGXBeingError) + t.Run("CancelledContext", testTransactionPGXCancelledContext) + t.Run("Panic", testTransactionPGXPanic) + t.Run("AnError", testTransactionPGXAnError) + t.Run("RollbackError", testTransactionPGXRollbackError) + t.Run("CommitError", testTransactionPGXCommitError) + t.Run("ShortStop", testTransactionPGXShortStop) + t.Run("RetrySuccess", testTransactionPGXRetrySuccess) + t.Run("MultipleFunctions", testTransactionPGXMultipleFunctions) + t.Run("RealDatabase", testTransactionPGXRealDatabase) +} + +func testTransactionPGXNilDatabase(t *testing.T) { + t.Parallel() + db := &mocks.DB{} + defer db.AssertExpectations(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + tr, err := dbtools.NewTransaction(db) + require.NoError(t, err) + + err = tr.PGX(ctx, func(pgx.Tx) error { + t.Error("didn't expect to receive this call") + return nil + }) + assertInError(t, err, dbtools.ErrEmptyDatabase) + + tr = &dbtools.Transaction{} + err = tr.PGX(ctx, func(pgx.Tx) error { + t.Error("didn't expect to receive this call") + return nil + }) + assertInError(t, err, dbtools.ErrEmptyDatabase) +} + +func testTransactionPGXBeingError(t *testing.T) { + t.Parallel() + db := &mocks.Pool{} + defer db.AssertExpectations(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + total := 3 + tr, err := dbtools.NewTransaction(db, dbtools.RetryCount(total)) + require.NoError(t, err) + + db.On("Begin", mock.Anything). + Return(nil, assert.AnError).Times(total) + + err = tr.PGX(ctx, func(pgx.Tx) error { + t.Error("didn't expect to receive this call") + return nil + }) + assertInError(t, err, assert.AnError) +} + +func testTransactionPGXCancelledContext(t *testing.T) { + t.Run("FirstFunction", testTransactionPGXCancelledContextFirstFunction) + t.Run("SecondFunction", testTransactionPGXCancelledContextSecondFunction) +} + +func testTransactionPGXCancelledContextFirstFunction(t *testing.T) { + t.Parallel() + db := &mocks.Pool{} + defer db.AssertExpectations(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + total := 4 + tr, err := dbtools.NewTransaction(db, dbtools.RetryCount(total*10)) + require.NoError(t, err) + + tx := &mocks.PGXTx{} + defer tx.AssertExpectations(t) + + db.On("Begin", mock.Anything).Return(tx, nil). + Times(total) + tx.On("Rollback", mock.Anything).Return(nil). + Times(total) + + calls := 0 + err = tr.PGX(ctx, func(pgx.Tx) error { + calls++ + // retry package stops it. + if calls >= total-1 { + cancel() + } + return assert.AnError + }) + assertInError(t, err, context.Canceled) + assert.Equal(t, total-1, calls) +} + +func testTransactionPGXCancelledContextSecondFunction(t *testing.T) { + t.Parallel() + db := &mocks.Pool{} + defer db.AssertExpectations(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + total := 4 + tr, err := dbtools.NewTransaction(db, dbtools.RetryCount(total*10)) + require.NoError(t, err) + + tx := &mocks.PGXTx{} + defer tx.AssertExpectations(t) + + db.On("Begin", mock.Anything).Return(tx, nil). + Times(total) + tx.On("Rollback", mock.Anything).Return(nil). + Times(total) + + calls := 0 + err = tr.PGX(ctx, func(pgx.Tx) error { + calls++ + // our loop catches it. + if calls >= total { + cancel() + } + return nil + }, func(pgx.Tx) error { + return assert.AnError + }) + assertInError(t, err, context.Canceled) + assert.Equal(t, total, calls) +} + +func testTransactionPGXPanic(t *testing.T) { + t.Parallel() + db := &mocks.Pool{} + defer db.AssertExpectations(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + total := 4 + tr, err := dbtools.NewTransaction(db, dbtools.RetryCount(total)) + require.NoError(t, err) + + tx := &mocks.PGXTx{} + defer tx.AssertExpectations(t) + + db.On("Begin", mock.Anything).Return(tx, nil). + Times(total) + tx.On("Rollback", mock.Anything).Return(nil). + Times(total) + + calls := 0 + err = tr.PGX(ctx, func(pgx.Tx) error { + calls++ + panic(assert.AnError.Error()) + }) + assertInError(t, err, assert.AnError) + assert.Equal(t, total, calls) +} + +func testTransactionPGXAnError(t *testing.T) { + t.Parallel() + db := &mocks.Pool{} + defer db.AssertExpectations(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + total := 4 + tr, err := dbtools.NewTransaction(db, dbtools.RetryCount(total)) + require.NoError(t, err) + + tx := &mocks.PGXTx{} + defer tx.AssertExpectations(t) + + db.On("Begin", mock.Anything).Return(tx, nil). + Times(total) + tx.On("Rollback", mock.Anything).Return(nil). + Times(total) + + calls := 0 + err = tr.PGX(ctx, func(pgx.Tx) error { + calls++ + return assert.AnError + }) + assertInError(t, err, assert.AnError) + assert.Equal(t, total, calls) +} + +func testTransactionPGXRollbackError(t *testing.T) { + t.Parallel() + db := &mocks.Pool{} + defer db.AssertExpectations(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + total := 4 + tr, err := dbtools.NewTransaction(db, dbtools.RetryCount(total)) + require.NoError(t, err) + + tx := &mocks.PGXTx{} + defer tx.AssertExpectations(t) + + db.On("Begin", mock.Anything).Return(tx, nil). + Times(total) + tx.On("Rollback", mock.Anything).Return(assert.AnError). + Times(total) + + calls := 0 + err = tr.PGX(ctx, func(pgx.Tx) error { + calls++ + panic(randomString(10)) + }) + assertInError(t, err, assert.AnError) + assert.Equal(t, total, calls) +} + +func testTransactionPGXCommitError(t *testing.T) { + t.Parallel() + db := &mocks.Pool{} + defer db.AssertExpectations(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + total := 4 + tr, err := dbtools.NewTransaction(db, dbtools.RetryCount(total)) + require.NoError(t, err) + + tx := &mocks.PGXTx{} + defer tx.AssertExpectations(t) + + db.On("Begin", mock.Anything).Return(tx, nil). + Times(total) + tx.On("Commit", mock.Anything).Return(assert.AnError). + Times(total) + + calls := 0 + err = tr.PGX(ctx, func(pgx.Tx) error { + calls++ + return nil + }) + assertInError(t, err, assert.AnError) + assert.Equal(t, total, calls) +} + +func testTransactionPGXShortStop(t *testing.T) { + t.Parallel() + db := &mocks.Pool{} + defer db.AssertExpectations(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + total := 3 + tr, err := dbtools.NewTransaction(db, dbtools.RetryCount(total*10)) + require.NoError(t, err) + + tx := &mocks.PGXTx{} + defer tx.AssertExpectations(t) + + db.On("Begin", mock.Anything).Return(tx, nil). + Times(total) + tx.On("Rollback", mock.Anything).Return(nil).Times(total) + + calls := 0 + err = tr.PGX(ctx, func(pgx.Tx) error { + calls++ + if calls >= total { + return retry.StopError{Err: assert.AnError} + } + return errors.New(randomString(10)) + }) + assertInError(t, err, assert.AnError) + assert.Equal(t, total, calls) +} + +func testTransactionPGXRetrySuccess(t *testing.T) { + t.Parallel() + db := &mocks.Pool{} + defer db.AssertExpectations(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + total := 4 + tr, err := dbtools.NewTransaction(db, dbtools.RetryCount(total*10)) + require.NoError(t, err) + + tx := &mocks.PGXTx{} + defer tx.AssertExpectations(t) + + db.On("Begin", mock.Anything).Return(tx, nil). + Times(total) + tx.On("Rollback", mock.Anything).Return(nil).Times(total - 1) + tx.On("Commit", mock.Anything).Return(nil).Once() + + calls := 0 + err = tr.PGX(ctx, func(pgx.Tx) error { + calls++ + if calls >= total { + return nil + } + return assert.AnError + }) + assert.NoError(t, err) + assert.Equal(t, total, calls) +} + +func testTransactionPGXMultipleFunctions(t *testing.T) { + t.Parallel() + db := &mocks.Pool{} + defer db.AssertExpectations(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + total := 4 + tr, err := dbtools.NewTransaction(db, dbtools.RetryCount(total*10)) + require.NoError(t, err) + + tx := &mocks.PGXTx{} + defer tx.AssertExpectations(t) + + db.On("Begin", mock.Anything).Return(tx, nil) + tx.On("Rollback", mock.Anything).Return(nil) + tx.On("Commit", mock.Anything).Return(nil).Once() + + callsFn1 := 0 + callsFn2 := 0 + err = tr.PGX(ctx, func(pgx.Tx) error { + callsFn1++ + if callsFn1 >= total { + return nil + } + return assert.AnError + }, func(pgx.Tx) error { + callsFn2++ + if callsFn2 >= 3 { + return nil + } + return assert.AnError + }) + assert.NoError(t, err) + assert.Equal(t, total+2, callsFn1, "expected three turns") + assert.Equal(t, 3, callsFn2) +} + +func testTransactionPGXRealDatabase(t *testing.T) { + t.Parallel() + if testing.Short() { + t.Skip("slow test") + } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + addr := getDB(t) + config, err := pgxpool.ParseConfig(addr) + require.NoError(t, err) + db, err := pgxpool.ConnectConfig(ctx, config) + require.NoError(t, err) + + tr, err := dbtools.NewTransaction(db, dbtools.RetryCount(10)) + require.NoError(t, err) + + names := []string{ + randomString(10), + randomString(20), + randomString(30), + } + gotNames := []string{} + err = tr.PGX(ctx, func(tx pgx.Tx) error { + query := `CREATE TABLE pgxtest (name VARCHAR(100) NOT NULL)` + _, err := tx.Exec(ctx, query) + return err + }, func(tx pgx.Tx) error { + query := `INSERT INTO pgxtest (name) VALUES ($1), ($2), ($3)` + _, err := tx.Exec(ctx, query, names[0], names[1], names[2]) + return err + }, func(tx pgx.Tx) error { + query := `SELECT name FROM pgxtest` + rows, err := tx.Query(ctx, query) + if err != nil { + return err + } + for rows.Next() { + var got string + err := rows.Scan(&got) + if err != nil { + return err + } + gotNames = append(gotNames, got) + } + return rows.Err() + }) + require.NoError(t, err) + assert.ElementsMatch(t, names, gotNames) +} + +func testTransactionDB(t *testing.T) { + t.Run("NilDatabase", testTransactionDBNilDatabase) + t.Run("BeingError", testTransactionDBBeingError) + t.Run("CancelledContext", testTransactionDBCancelledContext) + t.Run("Panic", testTransactionDBPanic) + t.Run("AnError", testTransactionDBAnError) + t.Run("RollbackError", testTransactionDBRollbackError) + t.Run("CommitError", testTransactionDBCommitError) + t.Run("ShortStop", testTransactionDBShortStop) + t.Run("RetrySuccess", testTransactionDBRetrySuccess) + t.Run("MultipleFunctions", testTransactionDBMultipleFunctions) + t.Run("RealDatabase", testTransactionDBRealDatabase) +} + +func testTransactionDBNilDatabase(t *testing.T) { + t.Parallel() + db := &mocks.Pool{} + defer db.AssertExpectations(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + tr, err := dbtools.NewTransaction(db) + require.NoError(t, err) + + err = tr.DB(ctx, func(dbtools.Tx) error { + t.Error("didn't expect to receive this call") + return nil + }) + assertInError(t, err, dbtools.ErrEmptyDatabase) + + tr = &dbtools.Transaction{} + err = tr.DB(ctx, func(dbtools.Tx) error { + t.Error("didn't expect to receive this call") + return nil + }) + assertInError(t, err, dbtools.ErrEmptyDatabase) +} + +func testTransactionDBBeingError(t *testing.T) { + t.Parallel() + db := &mocks.DB{} + defer db.AssertExpectations(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + total := 3 + tr, err := dbtools.NewTransaction(db, dbtools.RetryCount(total)) + require.NoError(t, err) + + db.On("BeginTx", mock.Anything, mock.Anything). + Return(nil, assert.AnError).Times(total) + + err = tr.DB(ctx, func(dbtools.Tx) error { + t.Error("didn't expect to receive this call") + return nil + }) + assertInError(t, err, assert.AnError) +} + +func testTransactionDBCancelledContext(t *testing.T) { + t.Run("FirstFunction", testTransactionDBCancelledContextFirstFunction) + t.Run("SecondFunction", testTransactionDBCancelledContextSecondFunction) +} + +func testTransactionDBCancelledContextFirstFunction(t *testing.T) { + t.Parallel() + db := &mocks.DB{} + defer db.AssertExpectations(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + total := 4 + tr, err := dbtools.NewTransaction(db, dbtools.RetryCount(total*10)) + require.NoError(t, err) + + tx := &mocks.Tx{} + defer tx.AssertExpectations(t) + + db.On("BeginTx", mock.Anything, mock.Anything).Return(tx, nil). + Times(total) + tx.On("Rollback").Return(nil). + Times(total) + + calls := 0 + err = tr.DB(ctx, func(dbtools.Tx) error { + calls++ + // retry package stops it. + if calls >= total-1 { + cancel() + } + return assert.AnError + }) + assertInError(t, err, context.Canceled) + assert.Equal(t, total-1, calls) +} + +func testTransactionDBCancelledContextSecondFunction(t *testing.T) { + t.Parallel() + db := &mocks.DB{} + defer db.AssertExpectations(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + total := 4 + tr, err := dbtools.NewTransaction(db, dbtools.RetryCount(total*10)) + require.NoError(t, err) + + tx := &mocks.Tx{} + defer tx.AssertExpectations(t) + + db.On("BeginTx", mock.Anything, mock.Anything).Return(tx, nil). + Times(total) + tx.On("Rollback").Return(nil). + Times(total) + + calls := 0 + err = tr.DB(ctx, func(dbtools.Tx) error { + calls++ + // our loop catches it. + if calls >= total { + cancel() + } + return nil + }, func(dbtools.Tx) error { + return assert.AnError + }) + assertInError(t, err, context.Canceled) + assert.Equal(t, total, calls) +} + +func testTransactionDBPanic(t *testing.T) { + t.Parallel() + db := &mocks.DB{} + defer db.AssertExpectations(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + total := 4 + tr, err := dbtools.NewTransaction(db, dbtools.RetryCount(total)) + require.NoError(t, err) + + tx := &mocks.Tx{} + defer tx.AssertExpectations(t) + + db.On("BeginTx", mock.Anything, mock.Anything).Return(tx, nil). + Times(total) + tx.On("Rollback").Return(nil). + Times(total) + + calls := 0 + err = tr.DB(ctx, func(dbtools.Tx) error { + calls++ + panic(assert.AnError.Error()) + }) + assertInError(t, err, assert.AnError) + assert.Equal(t, total, calls) +} + +func testTransactionDBAnError(t *testing.T) { + t.Parallel() + db := &mocks.DB{} + defer db.AssertExpectations(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + total := 4 + tr, err := dbtools.NewTransaction(db, dbtools.RetryCount(total)) + require.NoError(t, err) + + tx := &mocks.Tx{} + defer tx.AssertExpectations(t) + + db.On("BeginTx", mock.Anything, mock.Anything).Return(tx, nil). + Times(total) + tx.On("Rollback").Return(nil). + Times(total) + + calls := 0 + err = tr.DB(ctx, func(dbtools.Tx) error { + calls++ + return assert.AnError + }) + assertInError(t, err, assert.AnError) + assert.Equal(t, total, calls) +} + +func testTransactionDBRollbackError(t *testing.T) { + t.Parallel() + db := &mocks.DB{} + defer db.AssertExpectations(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + total := 4 + tr, err := dbtools.NewTransaction(db, dbtools.RetryCount(total)) + require.NoError(t, err) + + tx := &mocks.Tx{} + defer tx.AssertExpectations(t) + + db.On("BeginTx", mock.Anything, mock.Anything).Return(tx, nil). + Times(total) + tx.On("Rollback").Return(assert.AnError). + Times(total) + + calls := 0 + err = tr.DB(ctx, func(dbtools.Tx) error { + calls++ + panic(randomString(10)) + }) + assertInError(t, err, assert.AnError) + assert.Equal(t, total, calls) +} + +func testTransactionDBCommitError(t *testing.T) { + t.Parallel() + db := &mocks.DB{} + defer db.AssertExpectations(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + total := 4 + tr, err := dbtools.NewTransaction(db, dbtools.RetryCount(total)) + require.NoError(t, err) + + tx := &mocks.Tx{} + defer tx.AssertExpectations(t) + + db.On("BeginTx", mock.Anything, mock.Anything).Return(tx, nil). + Times(total) + tx.On("Commit").Return(assert.AnError). + Times(total) + + calls := 0 + err = tr.DB(ctx, func(dbtools.Tx) error { + calls++ + return nil + }) + assertInError(t, err, assert.AnError) + assert.Equal(t, total, calls) +} + +func testTransactionDBShortStop(t *testing.T) { + t.Parallel() + db := &mocks.DB{} + defer db.AssertExpectations(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + total := 3 + tr, err := dbtools.NewTransaction(db, dbtools.RetryCount(total*10)) + require.NoError(t, err) + + tx := &mocks.Tx{} + defer tx.AssertExpectations(t) + + db.On("BeginTx", mock.Anything, mock.Anything).Return(tx, nil). + Times(total) + tx.On("Rollback").Return(nil).Times(total) + + calls := 0 + err = tr.DB(ctx, func(dbtools.Tx) error { + calls++ + if calls >= total { + return retry.StopError{Err: assert.AnError} + } + return errors.New(randomString(10)) + }) + assertInError(t, err, assert.AnError) + assert.Equal(t, total, calls) +} + +func testTransactionDBRetrySuccess(t *testing.T) { + t.Parallel() + db := &mocks.DB{} + defer db.AssertExpectations(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + total := 4 + tr, err := dbtools.NewTransaction(db, dbtools.RetryCount(total*10)) + require.NoError(t, err) + + tx := &mocks.Tx{} + defer tx.AssertExpectations(t) + + db.On("BeginTx", mock.Anything, mock.Anything).Return(tx, nil). + Times(total) + tx.On("Rollback").Return(nil).Times(total - 1) + tx.On("Commit").Return(nil).Once() + + calls := 0 + err = tr.DB(ctx, func(dbtools.Tx) error { + calls++ + if calls >= total { + return nil + } + return assert.AnError + }) + assert.NoError(t, err) + assert.Equal(t, total, calls) +} + +func testTransactionDBMultipleFunctions(t *testing.T) { + t.Parallel() + db := &mocks.DB{} + defer db.AssertExpectations(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + total := 4 + tr, err := dbtools.NewTransaction(db, dbtools.RetryCount(total*10)) + require.NoError(t, err) + + tx := &mocks.Tx{} + defer tx.AssertExpectations(t) + + db.On("BeginTx", mock.Anything, mock.Anything).Return(tx, nil) + tx.On("Rollback").Return(nil) + tx.On("Commit").Return(nil).Once() + + callsFn1 := 0 + callsFn2 := 0 + err = tr.DB(ctx, func(dbtools.Tx) error { + callsFn1++ + if callsFn1 >= total { + return nil + } + return assert.AnError + }, func(dbtools.Tx) error { + callsFn2++ + if callsFn2 >= 3 { + return nil + } + return assert.AnError + }) + assert.NoError(t, err) + assert.Equal(t, total+2, callsFn1, "expected three turns") + assert.Equal(t, 3, callsFn2) +} + +func testTransactionDBRealDatabase(t *testing.T) { + t.Parallel() + if testing.Short() { + t.Skip("slow test") + } + addr := getDB(t) + config, err := pgx.ParseConfig(addr) + require.NoError(t, err) + db := stdlib.OpenDB(*config) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + tr, err := dbtools.NewTransaction(db, dbtools.RetryCount(10)) + require.NoError(t, err) + + names := []string{ + randomString(10), + randomString(20), + randomString(30), + } + gotNames := []string{} + err = tr.DB(ctx, func(tx dbtools.Tx) error { + query := `CREATE TABLE dbtest (name VARCHAR(100) NOT NULL)` + _, err := tx.ExecContext(ctx, query) + return err + }, func(tx dbtools.Tx) error { + query := `INSERT INTO dbtest (name) VALUES ($1), ($2), ($3)` + _, err := tx.ExecContext(ctx, query, names[0], names[1], names[2]) + return err + }, func(tx dbtools.Tx) error { + query := `SELECT name FROM dbtest` + rows, err := tx.QueryContext(ctx, query) + if err != nil { + return err + } + for rows.Next() { + var got string + err := rows.Scan(&got) + if err != nil { + return err + } + gotNames = append(gotNames, got) + } + return rows.Err() + }) + require.NoError(t, err) + assert.ElementsMatch(t, names, gotNames) +} + func TestWithTransaction(t *testing.T) { t.Run("BeginCommit", testWithTransactionBeginCommit) t.Run("RollbackFirst", testWithTransactionRollbackFirst) @@ -25,43 +849,43 @@ func TestWithTransaction(t *testing.T) { func testWithTransactionBeginCommit(t *testing.T) { t.Parallel() - db, mock, err := sqlmock.New() + db, dbMock, err := sqlmock.New() require.NoError(t, err) defer db.Close() defer func() { - if err := mock.ExpectationsWereMet(); err != nil { + if err := dbMock.ExpectationsWereMet(); err != nil { t.Errorf("there were unfulfilled expectations: %s", err) } }() - mock.ExpectBegin(). + dbMock.ExpectBegin(). WillReturnError(assert.AnError) err = dbtools.WithTransaction(db) assert.Equal(t, assert.AnError, errors.Cause(err)) - mock.ExpectBegin() - mock.ExpectCommit(). + dbMock.ExpectBegin() + dbMock.ExpectCommit(). WillReturnError(assert.AnError) err = dbtools.WithTransaction(db) assert.Equal(t, assert.AnError, errors.Cause(err)) - mock.ExpectBegin() - mock.ExpectCommit() + dbMock.ExpectBegin() + dbMock.ExpectCommit() err = dbtools.WithTransaction(db) assert.NoError(t, err) } func testWithTransactionRollbackFirst(t *testing.T) { t.Parallel() - db, mock, err := sqlmock.New() + db, dbMock, err := sqlmock.New() require.NoError(t, err) defer db.Close() defer func() { - if err := mock.ExpectationsWereMet(); err != nil { + if err := dbMock.ExpectationsWereMet(); err != nil { t.Errorf("there were unfulfilled expectations: %s", err) } }() - mock.ExpectBegin() - mock.ExpectRollback() + dbMock.ExpectBegin() + dbMock.ExpectRollback() err = dbtools.WithTransaction(db, func(*sql.Tx) error { return assert.AnError }, func(*sql.Tx) error { @@ -81,16 +905,16 @@ func testWithTransactionRollbackFirst(t *testing.T) { func testWithTransactionRollbackSecond(t *testing.T) { t.Parallel() - db, mock, err := sqlmock.New() + db, dbMock, err := sqlmock.New() require.NoError(t, err) defer db.Close() defer func() { - if err := mock.ExpectationsWereMet(); err != nil { + if err := dbMock.ExpectationsWereMet(); err != nil { t.Errorf("there were unfulfilled expectations: %s", err) } }() - mock.ExpectBegin() - mock.ExpectRollback() + dbMock.ExpectBegin() + dbMock.ExpectRollback() err = dbtools.WithTransaction(db, func(*sql.Tx) error { return nil }, func(*sql.Tx) error { @@ -109,23 +933,23 @@ func testWithTransactionRollbackSecond(t *testing.T) { func testWithTransactionCommit(t *testing.T) { t.Parallel() - db, mock, err := sqlmock.New() + db, dbMock, err := sqlmock.New() require.NoError(t, err) defer db.Close() defer func() { - if err := mock.ExpectationsWereMet(); err != nil { + if err := dbMock.ExpectationsWereMet(); err != nil { t.Errorf("there were unfulfilled expectations: %s", err) } }() - mock.ExpectBegin() - mock.ExpectCommit() + dbMock.ExpectBegin() + dbMock.ExpectCommit() err = dbtools.WithTransaction(db, func(*sql.Tx) error { return nil }) assert.NoError(t, err) - mock.ExpectBegin() - mock.ExpectCommit(). + dbMock.ExpectBegin() + dbMock.ExpectCommit(). WillReturnError(assert.AnError) err = dbtools.WithTransaction(db, func(*sql.Tx) error { return nil @@ -135,16 +959,16 @@ func testWithTransactionCommit(t *testing.T) { func testWithTransactionFunctionPanic(t *testing.T) { t.Parallel() - db, mock, err := sqlmock.New() + db, dbMock, err := sqlmock.New() require.NoError(t, err) defer db.Close() defer func() { - if err := mock.ExpectationsWereMet(); err != nil { + if err := dbMock.ExpectationsWereMet(); err != nil { t.Errorf("there were unfulfilled expectations: %s", err) } }() - mock.ExpectBegin() - mock.ExpectRollback() + dbMock.ExpectBegin() + dbMock.ExpectRollback() require.NotPanics(t, func() { err := dbtools.WithTransaction(db, func(*sql.Tx) error { panic("for some reason") @@ -159,18 +983,18 @@ func testWithTransactionFunctionPanic(t *testing.T) { func ExampleWithTransaction() { // For this example we are using sqlmock, but you can use an actual // connection with this function. - db, mock, err := sqlmock.New() + db, dbMock, err := sqlmock.New() if err != nil { panic(err) } defer db.Close() defer func() { - if err := mock.ExpectationsWereMet(); err != nil { + if err := dbMock.ExpectationsWereMet(); err != nil { fmt.Printf("there were unfulfilled expectations: %s\n", err) } }() - mock.ExpectBegin() - mock.ExpectCommit() + dbMock.ExpectBegin() + dbMock.ExpectCommit() err = dbtools.WithTransaction(db, func(*sql.Tx) error { fmt.Println("Running first query.") return nil @@ -189,18 +1013,18 @@ func ExampleWithTransaction() { func ExampleWithTransaction_two() { // For this example we are using sqlmock, but you can use an actual // connection with this function. - db, mock, err := sqlmock.New() + db, dbMock, err := sqlmock.New() if err != nil { panic(err) } defer db.Close() defer func() { - if err := mock.ExpectationsWereMet(); err != nil { + if err := dbMock.ExpectationsWereMet(); err != nil { fmt.Printf("there were unfulfilled expectations: %s\n", err) } }() - mock.ExpectBegin() - mock.ExpectRollback() + dbMock.ExpectBegin() + dbMock.ExpectRollback() err = dbtools.WithTransaction(db, func(*sql.Tx) error { fmt.Println("Running first query.") return nil @@ -316,11 +1140,11 @@ func testRetryTransactionRetry(t *testing.T) { func testRetryTransactionRetryZeroTimes(t *testing.T) { t.Parallel() - db, mock, err := sqlmock.New() + db, dbMock, err := sqlmock.New() require.NoError(t, err) defer db.Close() defer func() { - if err := mock.ExpectationsWereMet(); err != nil { + if err := dbMock.ExpectationsWereMet(); err != nil { t.Errorf("there were unfulfilled expectations: %s", err) } }() @@ -335,17 +1159,17 @@ func testRetryTransactionRetryZeroTimes(t *testing.T) { func testRetryTransactionRetryOnce(t *testing.T) { t.Parallel() - db, mock, err := sqlmock.New() + db, dbMock, err := sqlmock.New() require.NoError(t, err) defer db.Close() defer func() { - if err := mock.ExpectationsWereMet(); err != nil { + if err := dbMock.ExpectationsWereMet(); err != nil { t.Errorf("there were unfulfilled expectations: %s", err) } }() - mock.ExpectBegin() - mock.ExpectCommit() + dbMock.ExpectBegin() + dbMock.ExpectCommit() calls := 0 ctx := context.Background() @@ -359,19 +1183,19 @@ func testRetryTransactionRetryOnce(t *testing.T) { func testRetryTransactionRetryMoreTime(t *testing.T) { t.Parallel() - db, mock, err := sqlmock.New() + db, dbMock, err := sqlmock.New() require.NoError(t, err) defer db.Close() defer func() { - if err := mock.ExpectationsWereMet(); err != nil { + if err := dbMock.ExpectationsWereMet(); err != nil { t.Errorf("there were unfulfilled expectations: %s", err) } }() - mock.ExpectBegin() - mock.ExpectRollback() - mock.ExpectBegin() - mock.ExpectCommit() + dbMock.ExpectBegin() + dbMock.ExpectRollback() + dbMock.ExpectBegin() + dbMock.ExpectCommit() calls := 0 ctx := context.Background() @@ -389,23 +1213,23 @@ func testRetryTransactionRetryMoreTime(t *testing.T) { func ExampleRetryTransaction() { // For this example we are using sqlmock, but you can use an actual // connection with this function. - db, mock, err := sqlmock.New() + db, dbMock, err := sqlmock.New() if err != nil { panic(err) } defer db.Close() defer func() { - if err := mock.ExpectationsWereMet(); err != nil { + if err := dbMock.ExpectationsWereMet(); err != nil { fmt.Printf("there were unfulfilled expectations: %s\n", err) } }() - mock.ExpectBegin() - mock.ExpectRollback() - mock.ExpectBegin() - mock.ExpectRollback() - mock.ExpectBegin() - mock.ExpectCommit() + dbMock.ExpectBegin() + dbMock.ExpectRollback() + dbMock.ExpectBegin() + dbMock.ExpectRollback() + dbMock.ExpectBegin() + dbMock.ExpectCommit() calls := 0 ctx := context.Background() @@ -428,21 +1252,21 @@ func ExampleRetryTransaction() { func testRetryTransactionRetryError(t *testing.T) { t.Parallel() - db, mock, err := sqlmock.New() + db, dbMock, err := sqlmock.New() require.NoError(t, err) defer db.Close() defer func() { - if err := mock.ExpectationsWereMet(); err != nil { + if err := dbMock.ExpectationsWereMet(); err != nil { t.Errorf("there were unfulfilled expectations: %s", err) } }() - mock.ExpectBegin() - mock.ExpectRollback() - mock.ExpectBegin() - mock.ExpectRollback() - mock.ExpectBegin() - mock.ExpectRollback() + dbMock.ExpectBegin() + dbMock.ExpectRollback() + dbMock.ExpectBegin() + dbMock.ExpectRollback() + dbMock.ExpectBegin() + dbMock.ExpectRollback() ctx := context.Background() err = dbtools.RetryTransaction(ctx, db, 3, time.Nanosecond, func(*sql.Tx) error { @@ -467,11 +1291,11 @@ func testRetryTransactionContextCancelled(t *testing.T) { func testRetryTransactionContextCancelledAtBeginning(t *testing.T) { t.Parallel() - db, mock, err := sqlmock.New() + db, dbMock, err := sqlmock.New() require.NoError(t, err) defer db.Close() defer func() { - if err := mock.ExpectationsWereMet(); err != nil { + if err := dbMock.ExpectationsWereMet(); err != nil { t.Errorf("there were unfulfilled expectations: %s", err) } }() @@ -486,16 +1310,16 @@ func testRetryTransactionContextCancelledAtBeginning(t *testing.T) { func testRetryTransactionContextCancelledFirstRetry(t *testing.T) { t.Parallel() - db, mock, err := sqlmock.New() + db, dbMock, err := sqlmock.New() require.NoError(t, err) defer db.Close() defer func() { - if err := mock.ExpectationsWereMet(); err != nil { + if err := dbMock.ExpectationsWereMet(); err != nil { t.Errorf("there were unfulfilled expectations: %s", err) } }() - mock.ExpectBegin() - mock.ExpectRollback() + dbMock.ExpectBegin() + dbMock.ExpectRollback() ctx, cancel := context.WithCancel(context.Background()) defer cancel() called := false @@ -510,18 +1334,18 @@ func testRetryTransactionContextCancelledFirstRetry(t *testing.T) { func testRetryTransactionContextCancelledSecondRetry(t *testing.T) { t.Parallel() - db, mock, err := sqlmock.New() + db, dbMock, err := sqlmock.New() require.NoError(t, err) defer db.Close() defer func() { - if err := mock.ExpectationsWereMet(); err != nil { + if err := dbMock.ExpectationsWereMet(); err != nil { t.Errorf("there were unfulfilled expectations: %s", err) } }() - mock.ExpectBegin() - mock.ExpectRollback() - mock.ExpectBegin() - mock.ExpectRollback() + dbMock.ExpectBegin() + dbMock.ExpectRollback() + dbMock.ExpectBegin() + dbMock.ExpectRollback() ctx, cancel := context.WithCancel(context.Background()) defer cancel() calls := 0 diff --git a/dbtesting/dbtesting_test.go b/dbtesting/dbtesting_test.go index fda5c25..8d5b2ac 100644 --- a/dbtesting/dbtesting_test.go +++ b/dbtesting/dbtesting_test.go @@ -18,6 +18,7 @@ func TestOkValue(t *testing.T) { "byte slice": []byte("devil"), } for name, tc := range tcs { + tc := tc t.Run(name, func(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { diff --git a/dbtesting/mocha_test.go b/dbtesting/mocha_test.go index ac705e7..2c3f778 100644 --- a/dbtesting/mocha_test.go +++ b/dbtesting/mocha_test.go @@ -74,6 +74,7 @@ func testTerminalStartBuffer(t *testing.T) { }, } for name, tc := range tcs { + tc := tc t.Run(name, func(t *testing.T) { buf := &bytes.Buffer{} m := &dbtesting.Mocha{ @@ -122,6 +123,7 @@ func testTerminalSpecs(t *testing.T) { "skipped": "skipped: 4", } for name, tc := range tcs { + tc := tc t.Run(name, func(t *testing.T) { assert.Contains(t, content, tc) }) diff --git a/go.mod b/go.mod index e9e0373..b1351bf 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,20 @@ module github.com/arsham/dbtools -go 1.11 +go 1.14 require ( - github.com/DATA-DOG/go-sqlmock v1.3.3 - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/hashicorp/go-multierror v1.0.0 - github.com/pkg/errors v0.8.1 - github.com/sclevine/spec v1.3.0 - github.com/stretchr/testify v1.4.0 + github.com/DATA-DOG/go-sqlmock v1.5.0 + github.com/arsham/retry v0.1.1 + github.com/hashicorp/go-multierror v1.1.0 + github.com/jackc/pgconn v1.6.4 + github.com/jackc/pgproto3/v2 v2.0.4 // indirect + github.com/jackc/pgx/v4 v4.8.1 + github.com/pkg/errors v0.9.1 + github.com/sclevine/spec v1.4.0 + github.com/stretchr/objx v0.3.0 // indirect + github.com/stretchr/testify v1.6.1 + github.com/testcontainers/testcontainers-go v0.7.0 + golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect ) diff --git a/go.sum b/go.sum index 2b3be4b..8ab3e5c 100644 --- a/go.sum +++ b/go.sum @@ -1,22 +1,313 @@ -github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08= -github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= +github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/Microsoft/go-winio v0.4.11 h1:zoIOcVf0xPN1tnMVbTtEdI+P8OofVk3NObnwOQ6nK2Q= +github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= +github.com/Microsoft/hcsshim v0.8.6 h1:ZfF0+zZeYdzMIVMZHKtDKJvLHj76XCuVae/jNkjj0IA= +github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= +github.com/arsham/retry v0.1.1 h1:49OILWGAtTdFVQNi/yEmbfbc48zsSFVAqe8XtFhzcKo= +github.com/arsham/retry v0.1.1/go.mod h1:g6PJOHhR9WMEnzrmnRrDjGU7udI/Bh9x10Dg+C+0+cg= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc h1:TP+534wVlf61smEIq1nwLLAjQVEK2EADoW3CX9AuT+8= +github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 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/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible h1:dvc1KSkIYTVjZgHf/CTC2diTYC8PzhaA5sFISRfNVrE= +github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v0.7.3-0.20190506211059-b20a14b54661 h1:ZuxGvIvF01nfc/G9RJ5Q7Va1zQE2WJyG18Zv3DqCEf4= +github.com/docker/docker v0.7.3-0.20190506211059-b20a14b54661/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk= +github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-redis/redis v6.15.8+incompatible h1:BKZuG6mCnRj5AOaWJXoCgf6rqTYnYJLe4en2hxT7r9o= +github.com/go-redis/redis v6.15.8+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= +github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/protobuf v1.2.0 h1:xU6/SpYbvkNYiptHJYEDRseDLvYE7wSqhYYNy0QSUzI= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk= +github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= +github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= +github.com/jackc/pgconn v1.6.4 h1:S7T6cx5o2OqmxdHaXLH1ZeD1SbI8jBznyYE9Ec0RCQ8= +github.com/jackc/pgconn v1.6.4/go.mod h1:w2pne1C2tZgP+TvjqLpOigGzNqjBgQW9dUw/4Chex78= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2 h1:JVX6jT/XfzNqIjye4717ITLaNwV9mWbJx0dLCpcRzdA= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +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/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.0.2 h1:q1Hsy66zh4vuNsajBUF2PNqfAMMfxU5mk594lPE9vjY= +github.com/jackc/pgproto3/v2 v2.0.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.0.4 h1:RHkX5ZUD9bl/kn0f9dYUWs1N7Nwvo1wwUYvKiR26Zco= +github.com/jackc/pgproto3/v2 v2.0.4/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0= +github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po= +github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ= +github.com/jackc/pgtype v1.4.2 h1:t+6LWm5eWPLX1H5Se702JSBcirq6uWa4jiG4wV1rAWY= +github.com/jackc/pgtype v1.4.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA= +github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o= +github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg= +github.com/jackc/pgx/v4 v4.8.1 h1:SUbCLP2pXvf/Sr/25KsuI4aTxiFYIvpfk4l6aTSdyCw= +github.com/jackc/pgx/v4 v4.8.1/go.mod h1:4HOLxrl8wToZJReD04/yB20GDwf4KBYETvlHciCnwW0= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.1 h1:PJAw7H/9hoWC4Kf3J8iNmL1SwA6E8vfsLqBiL+F6CtI= +github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= +github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE= +github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= +github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y= +github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/sclevine/spec v1.3.0 h1:iTB51CYlnju5oRh0/l67fg1+RlQ2nqmFecwdvN+5TrI= -github.com/sclevine/spec v1.3.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8= +github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc h1:jUIKcSPO9MoMJBbEoyE/RJoE8vz7Mb8AjvifMMwSyvY= +github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As= +github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/testcontainers/testcontainers-go v0.7.0 h1:IaAsq5JY49GhDgCUKY87mo6JeOLOwp321iEP/SQjJKE= +github.com/testcontainers/testcontainers-go v0.7.0/go.mod h1:4dloDPrC94+8ebXA+Iei3Jy+gxF6uHQssJkB3mlP9Rg= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig= +golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180810170437-e96c4e24768d/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/grpc v1.17.0 h1:TRJYBgMclJvGYn2rIMjj+h9KtMt5r1Ij7ODVRIZkwhk= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= 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/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v0.0.0-20181223230014-1083505acf35 h1:zpdCK+REwbk+rqjJmHhiCN6iBIigrZ39glqSF0P3KF0= +gotest.tools v0.0.0-20181223230014-1083505acf35/go.mod h1:R//lfYlUuTOTfblYI3lGoAAAebUdzjvbmQsuB7Ykd90= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/helper_test.go b/helper_test.go new file mode 100644 index 0000000..9583f84 --- /dev/null +++ b/helper_test.go @@ -0,0 +1,133 @@ +package dbtools_test + +import ( + "context" + "fmt" + "math/rand" + "strings" + "testing" + "time" + + "github.com/arsham/dbtools/mocks" + "github.com/jackc/pgx/v4" + + "github.com/hashicorp/go-multierror" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +// assertInError returns true if the needle is found in stack, which is created +// either with pkg/errors help, the multierror or Go's error wrap. It will fall +// back to checking the contents of the needle.Error() is in haystack.Error(). +//nolint:unparam // I don't have a plan for this yet, but it remains. +func assertInError(t *testing.T, haystack, needle error) bool { + t.Helper() + if haystack == nil || needle == nil { + t.Errorf("want %v in %v", needle, haystack) + return false + } + if errors.Cause(haystack) == needle { + return true + } + if errors.Is(haystack, needle) { + return true + } + contains := func() bool { + return assert.Containsf(t, haystack.Error(), needle.Error(), + "want\n\t%v\nin\n\t%v", needle, haystack, + ) + } + merr, ok := errors.Cause(haystack).(*multierror.Error) + if !ok { + return contains() + } + for _, err := range merr.Errors { + if errors.Cause(err) == needle { + return true + } + } + return contains() +} + +func init() { + rand.Seed(time.Now().UnixNano()) +} + +func randomString(count int) string { + const runes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + b := make([]byte, count) + for i := range b { + b[i] = runes[rand.Intn(len(runes))] + } + return string(b) +} + +// getDB returns an address to a running postgres database inside a container. +// The container will be removed after test is finished running. +func getDB(t *testing.T) (addr string) { + t.Helper() + ctx := context.Background() + env := make(map[string]string) + env["POSTGRES_PASSWORD"] = "1234" + req := testcontainers.ContainerRequest{ + Image: "postgres:12-alpine", + ExposedPorts: []string{"5432/tcp"}, + WaitingFor: wait.ForListeningPort("5432/tcp"), + AutoRemove: true, + Env: env, + } + container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + require.NoError(t, err) + + ip, err := container.Host(ctx) + require.NoError(t, err) + + port, err := container.MappedPort(ctx, "5432") + require.NoError(t, err) + + t.Cleanup(func() { + container.Terminate(ctx) + }) + return buildQueryString("postgres", "1234", "postgres", ip, port.Port()) +} + +// buildQueryString builds a query string. +func buildQueryString(user, pass, dbname, host, port string) string { + parts := []string{} + if user != "" { + parts = append(parts, fmt.Sprintf("user=%s", user)) + } + if pass != "" { + parts = append(parts, fmt.Sprintf("password=%s", pass)) + } + if dbname != "" { + parts = append(parts, fmt.Sprintf("dbname=%s", dbname)) + } + if host != "" { + parts = append(parts, fmt.Sprintf("host=%s", host)) + } + if port != "" { + parts = append(parts, fmt.Sprintf("port=%s", port)) + } + return strings.Join(parts, " ") +} + +type exampleConn struct{} + +func (e *exampleConn) Begin(context.Context) (pgx.Tx, error) { + tx := &mocks.PGXTx{} + tx.On("Rollback", mock.Anything).Return(nil) + tx.On("Commit", mock.Anything).Return(nil) + return tx, nil +} + +func (e *exampleConn) BeginTx(ctx context.Context, _ pgx.TxOptions) (pgx.Tx, error) { + return e.Begin(ctx) +} diff --git a/mocks/db_mock.go b/mocks/db_mock.go new file mode 100644 index 0000000..a7f8ca1 --- /dev/null +++ b/mocks/db_mock.go @@ -0,0 +1,63 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + dbtools "github.com/arsham/dbtools" + mock "github.com/stretchr/testify/mock" + + sql "database/sql" +) + +// DB is an autogenerated mock type for the DB type +type DB struct { + mock.Mock +} + +// Begin provides a mock function with given fields: +func (_m *DB) Begin() (dbtools.Tx, error) { + ret := _m.Called() + + var r0 dbtools.Tx + if rf, ok := ret.Get(0).(func() dbtools.Tx); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(dbtools.Tx) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// BeginTx provides a mock function with given fields: ctx, opts +func (_m *DB) BeginTx(ctx context.Context, opts *sql.TxOptions) (dbtools.Tx, error) { + ret := _m.Called(ctx, opts) + + var r0 dbtools.Tx + if rf, ok := ret.Get(0).(func(context.Context, *sql.TxOptions) dbtools.Tx); ok { + r0 = rf(ctx, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(dbtools.Tx) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *sql.TxOptions) error); ok { + r1 = rf(ctx, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/mocks/pgx_tx_mock.go b/mocks/pgx_tx_mock.go new file mode 100644 index 0000000..512eeed --- /dev/null +++ b/mocks/pgx_tx_mock.go @@ -0,0 +1,230 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + pgconn "github.com/jackc/pgconn" + + pgx "github.com/jackc/pgx/v4" +) + +// PGXTx is an autogenerated mock type for the pgxTx type +type PGXTx struct { + mock.Mock +} + +// Begin provides a mock function with given fields: ctx +func (_m *PGXTx) Begin(ctx context.Context) (pgx.Tx, error) { + ret := _m.Called(ctx) + + var r0 pgx.Tx + if rf, ok := ret.Get(0).(func(context.Context) pgx.Tx); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(pgx.Tx) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Commit provides a mock function with given fields: ctx +func (_m *PGXTx) Commit(ctx context.Context) error { + ret := _m.Called(ctx) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Conn provides a mock function with given fields: +func (_m *PGXTx) Conn() *pgx.Conn { + ret := _m.Called() + + var r0 *pgx.Conn + if rf, ok := ret.Get(0).(func() *pgx.Conn); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*pgx.Conn) + } + } + + return r0 +} + +// CopyFrom provides a mock function with given fields: ctx, tableName, columnNames, rowSrc +func (_m *PGXTx) CopyFrom(ctx context.Context, tableName pgx.Identifier, columnNames []string, rowSrc pgx.CopyFromSource) (int64, error) { + ret := _m.Called(ctx, tableName, columnNames, rowSrc) + + var r0 int64 + if rf, ok := ret.Get(0).(func(context.Context, pgx.Identifier, []string, pgx.CopyFromSource) int64); ok { + r0 = rf(ctx, tableName, columnNames, rowSrc) + } else { + r0 = ret.Get(0).(int64) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, pgx.Identifier, []string, pgx.CopyFromSource) error); ok { + r1 = rf(ctx, tableName, columnNames, rowSrc) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Exec provides a mock function with given fields: ctx, sql, arguments +func (_m *PGXTx) Exec(ctx context.Context, sql string, arguments ...interface{}) (pgconn.CommandTag, error) { + var _ca []interface{} + _ca = append(_ca, ctx, sql) + _ca = append(_ca, arguments...) + ret := _m.Called(_ca...) + + var r0 pgconn.CommandTag + if rf, ok := ret.Get(0).(func(context.Context, string, ...interface{}) pgconn.CommandTag); ok { + r0 = rf(ctx, sql, arguments...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(pgconn.CommandTag) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, ...interface{}) error); ok { + r1 = rf(ctx, sql, arguments...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// LargeObjects provides a mock function with given fields: +func (_m *PGXTx) LargeObjects() pgx.LargeObjects { + ret := _m.Called() + + var r0 pgx.LargeObjects + if rf, ok := ret.Get(0).(func() pgx.LargeObjects); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(pgx.LargeObjects) + } + + return r0 +} + +// Prepare provides a mock function with given fields: ctx, name, sql +func (_m *PGXTx) Prepare(ctx context.Context, name string, sql string) (*pgconn.StatementDescription, error) { + ret := _m.Called(ctx, name, sql) + + var r0 *pgconn.StatementDescription + if rf, ok := ret.Get(0).(func(context.Context, string, string) *pgconn.StatementDescription); ok { + r0 = rf(ctx, name, sql) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*pgconn.StatementDescription) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, name, sql) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Query provides a mock function with given fields: ctx, sql, args +func (_m *PGXTx) Query(ctx context.Context, sql string, args ...interface{}) (pgx.Rows, error) { + var _ca []interface{} + _ca = append(_ca, ctx, sql) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + var r0 pgx.Rows + if rf, ok := ret.Get(0).(func(context.Context, string, ...interface{}) pgx.Rows); ok { + r0 = rf(ctx, sql, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(pgx.Rows) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, ...interface{}) error); ok { + r1 = rf(ctx, sql, args...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// QueryRow provides a mock function with given fields: ctx, sql, args +func (_m *PGXTx) QueryRow(ctx context.Context, sql string, args ...interface{}) pgx.Row { + var _ca []interface{} + _ca = append(_ca, ctx, sql) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + var r0 pgx.Row + if rf, ok := ret.Get(0).(func(context.Context, string, ...interface{}) pgx.Row); ok { + r0 = rf(ctx, sql, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(pgx.Row) + } + } + + return r0 +} + +// Rollback provides a mock function with given fields: ctx +func (_m *PGXTx) Rollback(ctx context.Context) error { + ret := _m.Called(ctx) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SendBatch provides a mock function with given fields: ctx, b +func (_m *PGXTx) SendBatch(ctx context.Context, b *pgx.Batch) pgx.BatchResults { + ret := _m.Called(ctx, b) + + var r0 pgx.BatchResults + if rf, ok := ret.Get(0).(func(context.Context, *pgx.Batch) pgx.BatchResults); ok { + r0 = rf(ctx, b) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(pgx.BatchResults) + } + } + + return r0 +} diff --git a/mocks/pool_mock.go b/mocks/pool_mock.go new file mode 100644 index 0000000..9c6e29e --- /dev/null +++ b/mocks/pool_mock.go @@ -0,0 +1,62 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + pgx "github.com/jackc/pgx/v4" +) + +// Pool is an autogenerated mock type for the Pool type +type Pool struct { + mock.Mock +} + +// Begin provides a mock function with given fields: ctx +func (_m *Pool) Begin(ctx context.Context) (pgx.Tx, error) { + ret := _m.Called(ctx) + + var r0 pgx.Tx + if rf, ok := ret.Get(0).(func(context.Context) pgx.Tx); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(pgx.Tx) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// BeginTx provides a mock function with given fields: ctx, txOptions +func (_m *Pool) BeginTx(ctx context.Context, txOptions pgx.TxOptions) (pgx.Tx, error) { + ret := _m.Called(ctx, txOptions) + + var r0 pgx.Tx + if rf, ok := ret.Get(0).(func(context.Context, pgx.TxOptions) pgx.Tx); ok { + r0 = rf(ctx, txOptions) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(pgx.Tx) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, pgx.TxOptions) error); ok { + r1 = rf(ctx, txOptions) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/mocks/tx_mock.go b/mocks/tx_mock.go new file mode 100644 index 0000000..a04d5e3 --- /dev/null +++ b/mocks/tx_mock.go @@ -0,0 +1,264 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + sql "database/sql" +) + +// Tx is an autogenerated mock type for the Tx type +type Tx struct { + mock.Mock +} + +// Commit provides a mock function with given fields: +func (_m *Tx) Commit() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Exec provides a mock function with given fields: query, args +func (_m *Tx) Exec(query string, args ...interface{}) (sql.Result, error) { + var _ca []interface{} + _ca = append(_ca, query) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + var r0 sql.Result + if rf, ok := ret.Get(0).(func(string, ...interface{}) sql.Result); ok { + r0 = rf(query, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(sql.Result) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, ...interface{}) error); ok { + r1 = rf(query, args...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ExecContext provides a mock function with given fields: ctx, query, args +func (_m *Tx) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) { + var _ca []interface{} + _ca = append(_ca, ctx, query) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + var r0 sql.Result + if rf, ok := ret.Get(0).(func(context.Context, string, ...interface{}) sql.Result); ok { + r0 = rf(ctx, query, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(sql.Result) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, ...interface{}) error); ok { + r1 = rf(ctx, query, args...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Prepare provides a mock function with given fields: query +func (_m *Tx) Prepare(query string) (*sql.Stmt, error) { + ret := _m.Called(query) + + var r0 *sql.Stmt + if rf, ok := ret.Get(0).(func(string) *sql.Stmt); ok { + r0 = rf(query) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*sql.Stmt) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// PrepareContext provides a mock function with given fields: ctx, query +func (_m *Tx) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) { + ret := _m.Called(ctx, query) + + var r0 *sql.Stmt + if rf, ok := ret.Get(0).(func(context.Context, string) *sql.Stmt); ok { + r0 = rf(ctx, query) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*sql.Stmt) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Query provides a mock function with given fields: query, args +func (_m *Tx) Query(query string, args ...interface{}) (*sql.Rows, error) { + var _ca []interface{} + _ca = append(_ca, query) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + var r0 *sql.Rows + if rf, ok := ret.Get(0).(func(string, ...interface{}) *sql.Rows); ok { + r0 = rf(query, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*sql.Rows) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, ...interface{}) error); ok { + r1 = rf(query, args...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// QueryContext provides a mock function with given fields: ctx, query, args +func (_m *Tx) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) { + var _ca []interface{} + _ca = append(_ca, ctx, query) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + var r0 *sql.Rows + if rf, ok := ret.Get(0).(func(context.Context, string, ...interface{}) *sql.Rows); ok { + r0 = rf(ctx, query, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*sql.Rows) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, ...interface{}) error); ok { + r1 = rf(ctx, query, args...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// QueryRow provides a mock function with given fields: query, args +func (_m *Tx) QueryRow(query string, args ...interface{}) *sql.Row { + var _ca []interface{} + _ca = append(_ca, query) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + var r0 *sql.Row + if rf, ok := ret.Get(0).(func(string, ...interface{}) *sql.Row); ok { + r0 = rf(query, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*sql.Row) + } + } + + return r0 +} + +// QueryRowContext provides a mock function with given fields: ctx, query, args +func (_m *Tx) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row { + var _ca []interface{} + _ca = append(_ca, ctx, query) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + var r0 *sql.Row + if rf, ok := ret.Get(0).(func(context.Context, string, ...interface{}) *sql.Row); ok { + r0 = rf(ctx, query, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*sql.Row) + } + } + + return r0 +} + +// Rollback provides a mock function with given fields: +func (_m *Tx) Rollback() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Stmt provides a mock function with given fields: stmt +func (_m *Tx) Stmt(stmt *sql.Stmt) *sql.Stmt { + ret := _m.Called(stmt) + + var r0 *sql.Stmt + if rf, ok := ret.Get(0).(func(*sql.Stmt) *sql.Stmt); ok { + r0 = rf(stmt) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*sql.Stmt) + } + } + + return r0 +} + +// StmtContext provides a mock function with given fields: ctx, stmt +func (_m *Tx) StmtContext(ctx context.Context, stmt *sql.Stmt) *sql.Stmt { + ret := _m.Called(ctx, stmt) + + var r0 *sql.Stmt + if rf, ok := ret.Get(0).(func(context.Context, *sql.Stmt) *sql.Stmt); ok { + r0 = rf(ctx, stmt) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*sql.Stmt) + } + } + + return r0 +}