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