diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 524f390..321bf88 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -19,7 +19,7 @@ jobs: steps: - uses: actions/setup-go@v3 with: - go-version: 1.20.7 + go-version: 1.21.0 - uses: actions/checkout@v3 - name: golangci-lint uses: golangci/golangci-lint-action@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0337738..f796363 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,8 +2,10 @@ name: Release on: push: + branches: + - main tags: - - "*" + - "v*.*.*" jobs: release: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4185673..a4a74f3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: 1.20.7 + go-version: 1.21.0 - name: Test run: go test -v ./... -cover -coverprofile cover.out && go tool cover -func cover.out diff --git a/Makefile b/Makefile index 59bd8c5..628e861 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,6 @@ -.PHONT: tools +TAG_TEMPLATE:= ^[v][0-9]+[.][0-9]+[.][0-9]([-]{0}|[-]{1}[0-9a-zA-Z]+[.]?[0-9a-zA-Z]+)+$$ + +PHONT: tools tools: ## Run tools (vet, gofmt, goimports, tidy, etc.) @go version gofmt -w . @@ -9,19 +11,25 @@ tools: ## Run tools (vet, gofmt, goimports, tidy, etc.) .PHONT: tools.update tools.update: ## Update or install tools go install golang.org/x/tools/cmd/goimports@latest + go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest .PHONT: deps.update -deps.update: ## Update dependencies versions - go get -u all - go mod tidy +deps.update: ## Update dependencies versions (root and sub modules) + @go get -u all + @go mod tidy + @for d in */ ; do pushd "$$d" && go get -u all && go mod tidy && popd; done + +.PHONT: go.sync +go.sync: ## Sync modules + @go work sync .PHONT: test test: ## Run tests with coverage - go test ./... -cover + @go test ./... -cover .PHONT: test.cover.all test.cover.all: ## Run tests with coverage (show all coverage) - go test -v ./... -cover -coverprofile cover.out && go tool cover -func cover.out + @go test -v ./... -cover -coverprofile cover.out && go tool cover -func cover.out .PHONY: lint lint: ## Run `golangci-lint` @@ -29,6 +37,24 @@ lint: ## Run `golangci-lint` @golangci-lint --version @golangci-lint run . +.PHONT: tags.add +tags.add: ## Set root module and submodules tags (git) + @(val=$$(echo $(t)| tr -d ' ') && \ + if [[ ! $$val =~ ${TAG_TEMPLATE} ]] ; then echo "not semantic version tag [$$val]" && exit 2; fi && \ + git tag "$$val" && echo "set root module's tag [$$val]" && \ + for d in */ ; do git tag "$$d$$val" && echo "set submodule's tag [$$d$$val]"; done) + +.PHONT: tags.del +tags.del: ## Delete root module and submodules tags (git) + @(val=$$(echo $(t)| tr -d ' ') && \ + if [[ ! $$val =~ ${TAG_TEMPLATE} ]] ; then echo "not semantic version tag [$$val]" && exit 2; fi && \ + git tag --delete "$$val" && echo "delete root module's tag [$$val]" && \ + for d in */ ; do git tag --delete "$$d$$val" && echo "delete submodule's tag [$$d$$val]"; done) + +.PHONT: tags.list +tags.list: ## List all exists tags (git) + @(git for-each-ref refs/tags --sort=-taggerdate --format='%(refname)') + .PHONY: help help: ## List all make targets with description @grep -h -E '^[.a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' diff --git a/Readme.md b/Readme.md index 8da64fb..da70828 100644 --- a/Readme.md +++ b/Readme.md @@ -1,4 +1,4 @@ -# OnionTx drawing +# oniontx drawing [![test](https://github.com/kozmod/oniontx/actions/workflows/test.yml/badge.svg)](https://github.com/kozmod/oniontx/actions/workflows/test.yml) [![Release](https://github.com/kozmod/oniontx/actions/workflows/release.yml/badge.svg)](https://github.com/kozmod/oniontx/actions/workflows/release.yml) ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/kozmod/oniontx) @@ -7,13 +7,14 @@ ![GitHub last commit](https://img.shields.io/github/last-commit/kozmod/oniontx) [![GitHub MIT license](https://img.shields.io/github/license/kozmod/oniontx)](https://github.com/kozmod/oniontx/blob/dev/LICENSE) -`OnionTx` allows to move transferring transaction management from the `Persistence` (repository) layer to the `Application` (service) layer using owner defined contract. +`oniotx` allows to move transferring transaction management from the `Persistence` (repository) layer to the `Application` (service) layer using owner defined contract. # drawing 🔴 **NOTE:** `Transactor` was designed to work with only the same instance of the "repository" (`*sql.DB`, etc.) ### The key features: - - [**`stdlib` implementation out of the box**](#stdlib) + - [**default implementation for `stdlib`**](#stdlib) + - [**default implementation for popular libraries**](#libs) - [**custom implementation's contract**](#custom) - - [**simple integration with popular libraries**](#integration_examples) + - [**simple testing with testing frameworks**](#testing) --- ### `stdlib` package @@ -29,14 +30,14 @@ import ( "log" "testing" - oniontx "github.com/kozmod/oniontx/stdlib" + ostdlib "github.com/kozmod/oniontx/stdlib" ) func main() { var ( db *sql.DB // database instance - tr = oniontx.NewTransactor(db) + tr = ostdlib.NewTransactor(db) r1 = repoA{t: tr} r2 = repoB{t: tr} ) @@ -62,7 +63,7 @@ func main() { } type repoA struct { - t *oniontx.Transactor + t *ostdlib.Transactor } func (r *repoA) InsertInTx(ctx context.Context, val string) error { @@ -75,7 +76,7 @@ func (r *repoA) InsertInTx(ctx context.Context, val string) error { } type repoB struct { - t *oniontx.Transactor + t *ostdlib.Transactor } func (r *repoB) InsertInTx(ctx context.Context, val string) error { @@ -87,13 +88,27 @@ func (r *repoB) InsertInTx(ctx context.Context, val string) error { return nil } ``` +[oniontx-examples](https://github.com/kozmod/oniontx-examples) contains more complicated +[example](https://github.com/kozmod/oniontx-examples/tree/master/internal/stdlib). + +--- +### Default implementation for database libs +`oniontx` has default implementation (as submodules) for maintaining transactions for database libraries: +[sqlx](https://github.com/jmoiron/sqlx), +[pgx](https://github.com/jackc/pgx), +[gorm](https://github.com/go-gorm/gorm). + +Examples: +- [sqlx](https://github.com/kozmod/oniontx-examples/tree/master/internal/sqlx) +- [pgx](https://github.com/kozmod/oniontx-examples/tree/master/internal/pgx) +- [gorm](https://github.com/kozmod/oniontx-examples/tree/master/internal/gorm) + --- -## Custom realisation -If it's required, `OnionTx` allowed opportunity to implements custom algorithms for maintaining transactions -(it is convenient if the persistence to DB implements not standard library: [sqlx](https://github.com/jmoiron/sqlx), [pgx](https://github.com/jackc/pgx), [gorm](https://github.com/go-gorm/gorm) etc. -Look at the [integration's examples](#integration_examples) section). -#### `OnitonTx` interfaces implementation +## Custom implementation +If it's required, `oniontx` allowed opportunity to implements custom algorithms for maintaining transactions (examples). + +#### Interfaces: ```go type ( // Mandatory @@ -123,9 +138,9 @@ type ( ) ``` ### Examples -***All examples based on `stdlib`.*** +`❗` ️***This examples based on `stdlib` pacakge.*** -`TxBeginner` and `Tx` implementations example: +`TxBeginner` and `Tx` implementations: ```go // Prepared contracts for execution package db @@ -169,7 +184,7 @@ func (t *Tx) Commit(_ context.Context) error { return t.Tx.Commit() } ``` -`Repositories` implementation example: +`Repositories` implementation: ```go package repoA @@ -228,7 +243,7 @@ func (r RepositoryB) Insert(ctx context.Context, val int) error { return nil } ``` -`UseCase` implementation example: +`UseCase` implementation: ```go package usecase @@ -272,7 +287,7 @@ func (s *UseCase) Exec(ctx context.Context, insert int) error { return nil } ``` -Configuring example: +Configuring: ```go package main @@ -280,8 +295,8 @@ import ( "context" "database/sql" "os" - - "github.com/kozmod/oniontx" + + oniontx "github.com/kozmod/oniontx" "github.com/user/some_project/internal/repoA" "github.com/user/some_project/internal/repoB" @@ -349,7 +364,7 @@ func WithIsolationLevel(level int) oniontx.Option[*sql.TxOptions] { } ``` -UsCase +UsCase: ```go func (s *Usecase) Do(ctx context.Context) error { err := s.Transactor.WithinTxWithOpts(ctx, func(ctx context.Context) error { @@ -373,7 +388,7 @@ func (s *Usecase) Do(ctx context.Context) error { #### Execution transaction in the different use cases ***Execution the same transaction for different `usecases` with the same `oniontx.Transactor` instance*** -UseCases +UseCases: ```go package a @@ -465,7 +480,7 @@ func (s *UseCaseB) Exec(ctx context.Context, insertA string, insertB int, delete return nil } ``` -Main +Main: ```go package main @@ -474,7 +489,7 @@ import ( "database/sql" "os" - "github.com/kozmod/oniontx" + oniontx "github.com/kozmod/oniontx" "github.com/user/some_project/internal/db" "github.com/user/some_project/internal/repoA" @@ -512,12 +527,8 @@ func main() { } ``` -### Integration's examples +### Testing -[oniontx-examples](https://github.com/kozmod/oniontx-examples) repository contains useful examples for integrations: +[oniontx-examples](https://github.com/kozmod/oniontx-examples) repository contains useful examples for creating unit test: -- [sqlx](https://github.com/kozmod/oniontx-examples/tree/master/internal/sqlx) -- [pgx](https://github.com/kozmod/oniontx-examples/tree/master/internal/pgx) -- [gorm](https://github.com/kozmod/oniontx-examples/tree/master/internal/gorm) -- [stdlib](https://github.com/kozmod/oniontx-examples/tree/master/internal/stdlib) -- [mockery](https://github.com/kozmod/oniontx-examples/tree/master/internal/mock/mockery) \ No newline at end of file +- [vektra/mockery **+** stretchr/testify](https://github.com/kozmod/oniontx-examples/tree/master/internal/mock/mockery) \ No newline at end of file diff --git a/go.mod b/go.mod index 9225c4b..bd7e654 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/kozmod/oniontx -go 1.20 +go 1.21.0 diff --git a/go.work b/go.work new file mode 100644 index 0000000..1a2126a --- /dev/null +++ b/go.work @@ -0,0 +1,9 @@ +go 1.21.0 + +use ( + . + gorm + pgx + sqlx + stdlib +) diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..9ef99b3 --- /dev/null +++ b/go.work.sum @@ -0,0 +1,20 @@ +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/gorm/Redme.md b/gorm/Redme.md new file mode 100644 index 0000000..7553c46 --- /dev/null +++ b/gorm/Redme.md @@ -0,0 +1,5 @@ +## gorm/oniontx + +Default implementation `gorm` of the `Transactor`. + +[Examples](https://github.com/kozmod/oniontx-examples/tree/master/internal/gorm) \ No newline at end of file diff --git a/gorm/go.mod b/gorm/go.mod new file mode 100644 index 0000000..1c99127 --- /dev/null +++ b/gorm/go.mod @@ -0,0 +1,15 @@ +module github.com/kozmod/oniontx/gorm + +go 1.21.0 + +replace github.com/kozmod/oniontx => ../ + +require ( + github.com/kozmod/oniontx v0.0.0 + gorm.io/gorm v1.25.7 +) + +require ( + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect +) diff --git a/gorm/go.sum b/gorm/go.sum new file mode 100644 index 0000000..7bfb41c --- /dev/null +++ b/gorm/go.sum @@ -0,0 +1,6 @@ +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= diff --git a/gorm/options.go b/gorm/options.go new file mode 100644 index 0000000..5254c37 --- /dev/null +++ b/gorm/options.go @@ -0,0 +1,33 @@ +package gorm + +import ( + "database/sql" + + "github.com/kozmod/oniontx" +) + +// TxOption implements oniontx.Option. +type TxOption func(opt *sql.TxOptions) + +// Apply the TxOption to [sql.TxOptions]. +func (o TxOption) Apply(opt *sql.TxOptions) { + o(opt) +} + +// WithReadOnly set `ReadOnly` sql.TxOptions option. +// +// Look at [sql.TxOptions.ReadOnly]. +func WithReadOnly(readonly bool) oniontx.Option[*sql.TxOptions] { + return TxOption(func(opt *sql.TxOptions) { + opt.ReadOnly = readonly + }) +} + +// WithIsolationLevel set sql.TxOptions isolation level. +// +// Look at [sql.TxOptions.Isolation]. +func WithIsolationLevel(level int) oniontx.Option[*sql.TxOptions] { + return TxOption(func(opt *sql.TxOptions) { + opt.Isolation = sql.IsolationLevel(level) + }) +} diff --git a/gorm/transactor.go b/gorm/transactor.go new file mode 100644 index 0000000..42445eb --- /dev/null +++ b/gorm/transactor.go @@ -0,0 +1,79 @@ +package gorm + +import ( + "context" + "database/sql" + + "gorm.io/gorm" + + "github.com/kozmod/oniontx" +) + +// dbWrapper wraps [gorm.DB] and implements [oniontx.TxBeginner]. +type dbWrapper struct { + *gorm.DB +} + +// BeginTx starts a transaction. +func (w *dbWrapper) BeginTx(_ context.Context, opts ...oniontx.Option[*sql.TxOptions]) (*dbWrapper, error) { + var txOptions sql.TxOptions + for _, opt := range opts { + opt.Apply(&txOptions) + } + tx := w.Begin(&txOptions) + return &dbWrapper{DB: tx}, nil +} + +// Rollback aborts the transaction. +func (w *dbWrapper) Rollback(_ context.Context) error { + tx := w.DB + tx.Rollback() + return nil +} + +// Commit commits the transaction. +func (w *dbWrapper) Commit(_ context.Context) error { + tx := w.DB + tx.Commit() + return nil +} + +// Transactor manage a transaction for single [gorm.DB] instance. +type Transactor struct { + *oniontx.Transactor[*dbWrapper, *dbWrapper, *sql.TxOptions] +} + +// NewTransactor returns new [Transactor] ([gorm] implementation). +func NewTransactor(db *gorm.DB) *Transactor { + var ( + base = dbWrapper{DB: db} + operator = oniontx.NewContextOperator[*dbWrapper, *dbWrapper](&base) + transactor = oniontx.NewTransactor[*dbWrapper, *dbWrapper, *sql.TxOptions](&base, operator) + ) + return &Transactor{ + Transactor: transactor, + } +} + +// TryGetTx returns pointer of [gorm.DB]([gorm.TxBeginner] or [gorm.ConnPoolBeginner]) and "true" from [context.Context] or return `false`. +func (t *Transactor) TryGetTx(ctx context.Context) (*gorm.DB, bool) { + wrapper, ok := t.Transactor.TryGetTx(ctx) + if !ok || wrapper == nil || wrapper.DB == nil { + return nil, false + } + return wrapper.DB, true +} + +// TxBeginner returns pointer of [gorm.DB]. +func (t *Transactor) TxBeginner() *gorm.DB { + return t.Transactor.TxBeginner().DB +} + +// GetExecutor returns pointer of [*gorm.DB] with transaction state. +func (t *Transactor) GetExecutor(ctx context.Context) *gorm.DB { + exec, ok := t.TryGetTx(ctx) + if !ok { + exec = t.TxBeginner() + } + return exec +} diff --git a/pgx/Redme.md b/pgx/Redme.md new file mode 100644 index 0000000..b8344d5 --- /dev/null +++ b/pgx/Redme.md @@ -0,0 +1,5 @@ +## pgx/oniontx + +Default implementation of the `Transactor` for `pgx` integration. + +[Examples](https://github.com/kozmod/oniontx-examples/tree/master/internal/pgx) \ No newline at end of file diff --git a/pgx/go.mod b/pgx/go.mod new file mode 100644 index 0000000..2ab7425 --- /dev/null +++ b/pgx/go.mod @@ -0,0 +1,17 @@ +module github.com/kozmod/oniontx/pgx + +go 1.21.0 + +replace github.com/kozmod/oniontx => ../ + +require ( + github.com/jackc/pgx/v5 v5.5.3 + github.com/kozmod/oniontx v0.0.0 +) + +require ( + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect + golang.org/x/crypto v0.20.0 // indirect + golang.org/x/text v0.14.0 // indirect +) diff --git a/pgx/go.sum b/pgx/go.sum new file mode 100644 index 0000000..d5ec929 --- /dev/null +++ b/pgx/go.sum @@ -0,0 +1,23 @@ +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/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= +github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.3 h1:Ces6/M3wbDXYpM8JyyPD57ivTtJACFZJd885pdIaV2s= +github.com/jackc/pgx/v5 v5.5.3/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg= +golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/pgx/options.go b/pgx/options.go new file mode 100644 index 0000000..e2dfa54 --- /dev/null +++ b/pgx/options.go @@ -0,0 +1,40 @@ +package pgx + +import ( + "github.com/jackc/pgx/v5" + + "github.com/kozmod/oniontx" +) + +// TxOption implements oniontx.Option. +type TxOption func(opt *pgx.TxOptions) + +// Apply the TxOption to [sql.TxOptions]. +func (o TxOption) Apply(opt *pgx.TxOptions) { + o(opt) +} + +// WithAccessMode - +func WithAccessMode(mode pgx.TxAccessMode) oniontx.Option[*pgx.TxOptions] { + return TxOption(func(opt *pgx.TxOptions) { + opt.AccessMode = mode + }) +} + +func WithDeferrableMode(mode pgx.TxDeferrableMode) oniontx.Option[*pgx.TxOptions] { + return TxOption(func(opt *pgx.TxOptions) { + opt.DeferrableMode = mode + }) +} + +func WithIsoLevel(lvl pgx.TxIsoLevel) oniontx.Option[*pgx.TxOptions] { + return TxOption(func(opt *pgx.TxOptions) { + opt.IsoLevel = lvl + }) +} + +func WithBeginQuery(beginQuery string) oniontx.Option[*pgx.TxOptions] { + return TxOption(func(opt *pgx.TxOptions) { + opt.BeginQuery = beginQuery + }) +} diff --git a/pgx/transactor.go b/pgx/transactor.go new file mode 100644 index 0000000..0492775 --- /dev/null +++ b/pgx/transactor.go @@ -0,0 +1,87 @@ +package pgx + +import ( + "context" + "github.com/jackc/pgx/v5/pgconn" + + "github.com/jackc/pgx/v5" + + "github.com/kozmod/oniontx" +) + +// Executor represents common methods of [pgx.Conn] and [pgx.Tx]. +type Executor interface { + Exec(ctx context.Context, sql string, arguments ...any) (commandTag pgconn.CommandTag, err error) + Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error) + QueryRow(ctx context.Context, sql string, args ...any) pgx.Row + Prepare(ctx context.Context, name, sql string) (sd *pgconn.StatementDescription, err error) +} + +// dbWrapper wraps [pgx.Conn] and implements [oniontx.TxBeginner]. +type dbWrapper struct { + *pgx.Conn +} + +// BeginTx starts a transaction. +func (w *dbWrapper) BeginTx(ctx context.Context, opts ...oniontx.Option[*pgx.TxOptions]) (*txWrapper, error) { + var txOptions pgx.TxOptions + for _, opt := range opts { + opt.Apply(&txOptions) + } + tx, err := w.Conn.BeginTx(ctx, txOptions) + return &txWrapper{Tx: tx}, err +} + +// txWrapper wraps [pgx.Tx] and implements [oniontx.Tx] +type txWrapper struct { + pgx.Tx +} + +// Rollback aborts the transaction. +func (t *txWrapper) Rollback(ctx context.Context) error { + return t.Tx.Rollback(ctx) +} + +// Commit commits the transaction. +func (t *txWrapper) Commit(ctx context.Context) error { + return t.Tx.Commit(ctx) +} + +// Transactor manage a transaction for single [pgx.Conn] instance. +type Transactor struct { + *oniontx.Transactor[*dbWrapper, *txWrapper, *pgx.TxOptions] +} + +// NewTransactor returns new Transactor ([pgx] implementation). +func NewTransactor(conn *pgx.Conn) *Transactor { + var ( + base = dbWrapper{Conn: conn} + operator = oniontx.NewContextOperator[*dbWrapper, *txWrapper](&base) + transactor = oniontx.NewTransactor[*dbWrapper, *txWrapper, *pgx.TxOptions](&base, operator) + ) + return &Transactor{ + Transactor: transactor, + } +} + +// TryGetTx returns pointer of [pgx.Tx] and "true" from [context.Context] or return `false`. +func (t *Transactor) TryGetTx(ctx context.Context) (pgx.Tx, bool) { + wrapper, ok := t.Transactor.TryGetTx(ctx) + if !ok || wrapper == nil || wrapper.Tx == nil { + return nil, false + } + return wrapper.Tx, true +} + +// TxBeginner returns pointer of [pgx.Conn]. +func (t *Transactor) TxBeginner() *pgx.Conn { + return t.Transactor.TxBeginner().Conn +} + +// GetExecutor returns Executor implementation ([*pgx.Conn] or [*pgx.Tx] default wrappers). +func (t *Transactor) GetExecutor(ctx context.Context) Executor { + if tx, ok := t.TryGetTx(ctx); ok { + return tx + } + return t.TxBeginner() +} diff --git a/sqlx/Redme.md b/sqlx/Redme.md new file mode 100644 index 0000000..509e3fb --- /dev/null +++ b/sqlx/Redme.md @@ -0,0 +1,5 @@ +## sqlx/oniontx + +Default implementation of the `Transactor` for `sqlx` integration. + +[Examples](https://github.com/kozmod/oniontx-examples/tree/master/internal/sqlx) \ No newline at end of file diff --git a/sqlx/go.mod b/sqlx/go.mod new file mode 100644 index 0000000..b7f8760 --- /dev/null +++ b/sqlx/go.mod @@ -0,0 +1,11 @@ +module github.com/kozmod/oniontx/sqlx + +go 1.21.0 + +replace github.com/kozmod/oniontx => ../ + +require ( + github.com/jmoiron/sqlx v1.3.5 + github.com/kozmod/oniontx v0.0.0 + github.com/lib/pq v1.10.9 +) diff --git a/sqlx/go.sum b/sqlx/go.sum new file mode 100644 index 0000000..61ef65b --- /dev/null +++ b/sqlx/go.sum @@ -0,0 +1,9 @@ +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= diff --git a/sqlx/options.go b/sqlx/options.go new file mode 100644 index 0000000..61641d5 --- /dev/null +++ b/sqlx/options.go @@ -0,0 +1,33 @@ +package sqlx + +import ( + "database/sql" + + "github.com/kozmod/oniontx" +) + +// TxOption implements oniontx.Option. +type TxOption func(opt *sql.TxOptions) + +// Apply the TxOption to [sql.TxOptions]. +func (o TxOption) Apply(opt *sql.TxOptions) { + o(opt) +} + +// WithReadOnly set `ReadOnly` sql.TxOptions option. +// +// Look at [sql.TxOptions.ReadOnly]. +func WithReadOnly(readonly bool) oniontx.Option[*sql.TxOptions] { + return TxOption(func(opt *sql.TxOptions) { + opt.ReadOnly = readonly + }) +} + +// WithIsolationLevel set sql.TxOptions isolation level. +// +// Look at [sql.TxOptions.Isolation]. +func WithIsolationLevel(level int) oniontx.Option[*sql.TxOptions] { + return TxOption(func(opt *sql.TxOptions) { + opt.Isolation = sql.IsolationLevel(level) + }) +} diff --git a/sqlx/transactor.go b/sqlx/transactor.go new file mode 100644 index 0000000..c9398c1 --- /dev/null +++ b/sqlx/transactor.go @@ -0,0 +1,91 @@ +package sqlx + +import ( + "context" + "database/sql" + _ "github.com/lib/pq" + + "github.com/jmoiron/sqlx" + "github.com/kozmod/oniontx" +) + +// Executor represents common methods of [sqlx.DB] and [sqlx.Tx]. +type Executor interface { + Exec(query string, args ...any) (sql.Result, error) + ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) + Query(query string, args ...any) (*sql.Rows, error) + QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) + QueryRow(query string, args ...any) *sql.Row + QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row + Prepare(query string) (*sql.Stmt, error) + PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) +} + +// dbWrapper wraps [sqlx.DB] and implements [oniontx.TxBeginner]. +type dbWrapper struct { + *sqlx.DB +} + +// BeginTx starts a transaction. +func (w *dbWrapper) BeginTx(ctx context.Context, opts ...oniontx.Option[*sql.TxOptions]) (*txWrapper, error) { + var txOptions sql.TxOptions + for _, opt := range opts { + opt.Apply(&txOptions) + } + tx, err := w.DB.BeginTxx(ctx, &txOptions) + return &txWrapper{Tx: tx}, err +} + +// txWrapper wraps [sqlx.Tx] and implements [oniontx.Tx] +type txWrapper struct { + *sqlx.Tx +} + +// Rollback aborts the transaction. +func (t *txWrapper) Rollback(_ context.Context) error { + return t.Tx.Rollback() +} + +// Commit commits the transaction. +func (t *txWrapper) Commit(_ context.Context) error { + return t.Tx.Commit() +} + +// Transactor manage a transaction for single [pgx.Conn] instance. +type Transactor struct { + *oniontx.Transactor[*dbWrapper, *txWrapper, *sql.TxOptions] +} + +// NewTransactor returns new Transactor ([sqlx] implementation). +func NewTransactor(db *sqlx.DB) *Transactor { + var ( + base = dbWrapper{DB: db} + operator = oniontx.NewContextOperator[*dbWrapper, *txWrapper](&base) + transactor = oniontx.NewTransactor[*dbWrapper, *txWrapper, *sql.TxOptions](&base, operator) + ) + return &Transactor{ + Transactor: transactor, + } +} + +// TryGetTx returns pointer of [sqlx.Tx] and "true" from [context.Context] or return `false`. +func (t *Transactor) TryGetTx(ctx context.Context) (*sqlx.Tx, bool) { + wrapper, ok := t.Transactor.TryGetTx(ctx) + if !ok || wrapper == nil || wrapper.Tx == nil { + return nil, false + } + return wrapper.Tx, true +} + +// TxBeginner returns pointer of [sqlx.DB]. +func (t *Transactor) TxBeginner() *sqlx.DB { + return t.Transactor.TxBeginner().DB +} + +// GetExecutor returns Executor implementation ([*sqlx.DB] or [*sqlx.Tx] default wrappers). +func (t *Transactor) GetExecutor(ctx context.Context) Executor { + if tx, ok := t.TryGetTx(ctx); ok { + return tx + } + return t.TxBeginner() +} diff --git a/stdlib/Redme.md b/stdlib/Redme.md new file mode 100644 index 0000000..2d1448a --- /dev/null +++ b/stdlib/Redme.md @@ -0,0 +1,5 @@ +## stdlib/oniontx + +Default implementation of the `Transactor` for `stdlib` integration. + +[Examples](https://github.com/kozmod/oniontx-examples/tree/master/internal/stdlib) \ No newline at end of file diff --git a/stdlib/go.mod b/stdlib/go.mod new file mode 100644 index 0000000..240627f --- /dev/null +++ b/stdlib/go.mod @@ -0,0 +1,7 @@ +module github.com/kozmod/oniontx/stdlib + +go 1.21.0 + +replace github.com/kozmod/oniontx => ../ + +require github.com/kozmod/oniontx v0.0.0 diff --git a/stdlib/transactor.go b/stdlib/transactor.go index 4cf633a..1080bb5 100644 --- a/stdlib/transactor.go +++ b/stdlib/transactor.go @@ -19,7 +19,7 @@ type Executor interface { PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) } -// dbWrapper wraps sql.DB and implements oniontx.TxBeginner. +// dbWrapper wraps [sql.DB] and implements [oniontx.TxBeginner]. type dbWrapper struct { *sql.DB } @@ -34,7 +34,7 @@ func (db dbWrapper) BeginTx(ctx context.Context, opts ...oniontx.Option[*sql.TxO return &txWrapper{Tx: tx}, err } -// txWrapper wraps sql.Tx and implements oniontx.Tx. +// txWrapper wraps [sql.Tx] and implements [oniontx.Tx]. type txWrapper struct { *sql.Tx } @@ -49,61 +49,56 @@ func (t *txWrapper) Commit(_ context.Context) error { return t.Tx.Commit() } -// Transactor manage a transaction for single sql.DB instance. +// Transactor manage a transaction for single [sql.DB] instance. type Transactor struct { - transactor *oniontx.Transactor[*dbWrapper, *txWrapper, *sql.TxOptions] - operator *oniontx.ContextOperator[*dbWrapper, *txWrapper] + *oniontx.Transactor[*dbWrapper, *txWrapper, *sql.TxOptions] } -// NewTransactor returns new Transactor. +// NewTransactor returns new [Transactor]. func NewTransactor(db *sql.DB) *Transactor { var ( base = dbWrapper{DB: db} operator = oniontx.NewContextOperator[*dbWrapper, *txWrapper](&base) transactor = Transactor{ - operator: operator, - transactor: oniontx.NewTransactor[*dbWrapper, *txWrapper, *sql.TxOptions]( - &base, - operator, - ), + Transactor: oniontx.NewTransactor[*dbWrapper, *txWrapper, *sql.TxOptions](&base, operator), } ) return &transactor } -// WithinTx execute all queries with sql.Tx. +// WithinTx execute all queries with [sql.Tx]. // -// The function create new sql.Tx or reuse sql.Tx obtained from [context.Context]. +// Creates new [sql.Tx] or reuse [sql.Tx] obtained from [context.Context]. func (t *Transactor) WithinTx(ctx context.Context, fn func(ctx context.Context) error) (err error) { - return t.transactor.WithinTx(ctx, fn) + return t.Transactor.WithinTx(ctx, fn) } -// WithinTxWithOpts execute all queries with sql.Tx and transaction sql.TxOptions. +// WithinTxWithOpts execute all queries with [sql.Tx] and transaction [sql.TxOptions]. // -// The function create new sql.Tx or reuse sql.Tx obtained from [context.Context]. +// Creates new [sql.Tx] or reuse [sql.Tx] obtained from [context.Context]. func (t *Transactor) WithinTxWithOpts(ctx context.Context, fn func(ctx context.Context) error, opts ...oniontx.Option[*sql.TxOptions]) (err error) { - return t.transactor.WithinTxWithOpts(ctx, fn, opts...) + return t.Transactor.WithinTxWithOpts(ctx, fn, opts...) } -// TryGetTx returns pointer of sql.Tx and "true" from [context.Context] or return `false`. +// TryGetTx returns pointer of [sql.Tx] and "true" from [context.Context] or return `false`. func (t *Transactor) TryGetTx(ctx context.Context) (*sql.Tx, bool) { - wrapper, ok := t.transactor.TryGetTx(ctx) + wrapper, ok := t.Transactor.TryGetTx(ctx) if !ok || wrapper == nil || wrapper.Tx == nil { return nil, false } return wrapper.Tx, true } -// TxBeginner returns pointer of sql.DB. +// TxBeginner returns pointer of [sql.DB]. func (t *Transactor) TxBeginner() *sql.DB { - return t.transactor.TxBeginner().DB + return t.Transactor.TxBeginner().DB } -// GetExecutor returns Executor implementation (sql.DB or sql.Tx). +// GetExecutor returns [Executor] implementation ([*sql.DB] or [*sql.Tx] default wrappers). func (t *Transactor) GetExecutor(ctx context.Context) Executor { - tx, ok := t.operator.Extract(ctx) + tx, ok := t.Transactor.TryGetTx(ctx) if !ok { - return t.transactor.TxBeginner() + return t.Transactor.TxBeginner() } return tx }