├── LICENSE ├── README.md ├── context.go ├── go.mod ├── go.sum ├── pool.go ├── query.go └── runner.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Malykh Alex 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pgxatomic 2 | 3 | pgxatomic is a library of tools that allow you to implement transfer of clean control to transactions to a higher level by adding transaction in a `context.Context` using [pgx](https://github.com/jackc/pgx) driver. 4 | 5 | ![schema](https://i.imgur.com/RpsfuBb.jpg) 6 | 7 | ## Example Usage 8 | 1. You can use `pgxatomic.Pool` within repository implementation. It's simple wrapper around `pgxpool.Pool` which 9 | is wrapping `Query`, `QueryRow` and `Exec` methods with `pgxatomic` query functions. 10 | ```go 11 | type orderRepo struct { 12 | pool *pgxatomic.Pool // pgxpool.Pool wrapper 13 | } 14 | 15 | type order struct { 16 | ID uuid.UUID 17 | Cost int 18 | } 19 | 20 | func (r *orderRepo) Insert(ctx context.Context, cost int) order { 21 | rows, _ := r.pool.Query(ctx, "insert into order(cost) values ($1) RETURNING id, cost", cost) 22 | o, _ := pgx.CollectOneRow(rows, pgx.RowToStructByPos[order]) 23 | return o 24 | } 25 | ``` 26 | 27 | Or you can use `Query`, `QueryRow`, `Exec` functions directly from the library. 28 | 29 | 2. Run wrapped usecase method calls within txFunc using `pgxatomic.runner.Run` function 30 | ```go 31 | conf, _ := pgxpool.ParseConfig("postgres://user:pass@localhost:5432/postgres") 32 | pool, _ := pgxpool.NewWithConfig(context.Background(), conf) 33 | 34 | r, _ := pgxatomic.NewRunner(pool, pgx.TxOptions{}) 35 | 36 | _ = r.Run(context.Background(), func(txCtx context.Context) error { 37 | _ = orderService.Create(txCtx) 38 | _ = balanceService.Withdraw(txCtx) 39 | return nil 40 | }) 41 | ``` 42 | 43 | Error handling is omitted on purpose, handle all errors! 44 | 45 | ## Credits 46 | - [Clean transactions in Golang hexagon](https://www.kaznacheev.me/posts/en/clean-transactions-in-hexagon) 47 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package pgxatomic 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/jackc/pgx/v5" 7 | ) 8 | 9 | type txKey struct{} 10 | 11 | // withTx sets pgx.Tx into context. 12 | func withTx(ctx context.Context, tx pgx.Tx) context.Context { 13 | return context.WithValue(ctx, txKey{}, tx) 14 | } 15 | 16 | // txFromContext return pgx.Tx from context or nil if not found. 17 | func txFromContext(ctx context.Context) pgx.Tx { 18 | if tx, ok := ctx.Value(txKey{}).(pgx.Tx); ok { 19 | return tx 20 | } 21 | return nil 22 | } 23 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ysomad/pgxatomic 2 | 3 | go 1.19 4 | 5 | require github.com/jackc/pgx/v5 v5.0.4 6 | 7 | require ( 8 | github.com/jackc/pgpassfile v1.0.0 // indirect 9 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect 10 | github.com/jackc/puddle/v2 v2.0.0 // indirect 11 | golang.org/x/crypto v0.1.0 // indirect 12 | golang.org/x/text v0.4.0 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 4 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 5 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= 6 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 7 | github.com/jackc/pgx/v5 v5.0.4 h1:r5O6y84qHX/z/HZV40JBdx2obsHz7/uRj5b+CcYEdeY= 8 | github.com/jackc/pgx/v5 v5.0.4/go.mod h1:U0ynklHtgg43fue9Ly30w3OCSTDPlXjig9ghrNGaguQ= 9 | github.com/jackc/puddle/v2 v2.0.0 h1:Kwk/AlLigcnZsDssc3Zun1dk1tAtQNPaBBxBHWn0Mjc= 10 | github.com/jackc/puddle/v2 v2.0.0/go.mod h1:itE7ZJY8xnoo0JqJEpSMprN0f+NQkMCuEV/N9j8h0oc= 11 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 13 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 14 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 15 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 16 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 17 | golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= 18 | golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= 19 | golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= 20 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 21 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 22 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 23 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 24 | -------------------------------------------------------------------------------- /pool.go: -------------------------------------------------------------------------------- 1 | package pgxatomic 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/jackc/pgx/v5" 8 | "github.com/jackc/pgx/v5/pgconn" 9 | "github.com/jackc/pgx/v5/pgxpool" 10 | ) 11 | 12 | // Pool wraps pgxpool.Pool query methods with pgxatomic corresponding functions 13 | // which injects pgx.Tx into context. 14 | type Pool struct { 15 | p *pgxpool.Pool 16 | } 17 | 18 | func NewPool(p *pgxpool.Pool) (Pool, error) { 19 | if p == nil { 20 | return Pool{}, errors.New("pgxatomic: pool cannot be nil") 21 | } 22 | return Pool{p: p}, nil 23 | } 24 | 25 | func (p Pool) Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error) { 26 | return Query(ctx, p.p, sql, args...) 27 | } 28 | 29 | func (p Pool) QueryRow(ctx context.Context, sql string, args ...any) pgx.Row { 30 | return QueryRow(ctx, p.p, sql, args...) 31 | } 32 | 33 | func (p Pool) Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) { 34 | return Exec(ctx, p.p, sql, args...) 35 | } 36 | -------------------------------------------------------------------------------- /query.go: -------------------------------------------------------------------------------- 1 | package pgxatomic 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/jackc/pgx/v5" 7 | "github.com/jackc/pgx/v5/pgconn" 8 | ) 9 | 10 | // Query is a wrapper around pgx Query method. 11 | func Query( 12 | ctx context.Context, 13 | db interface { 14 | Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error) 15 | }, 16 | sql string, 17 | args ...any, 18 | ) (pgx.Rows, error) { 19 | if tx := txFromContext(ctx); tx != nil { 20 | return tx.Query(ctx, sql, args...) 21 | } 22 | return db.Query(ctx, sql, args...) 23 | } 24 | 25 | // Exec is a wrapper around pgx Exec method. 26 | func Exec( 27 | ctx context.Context, 28 | db interface { 29 | Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) 30 | }, 31 | sql string, 32 | args ...any, 33 | ) (pgconn.CommandTag, error) { 34 | if tx := txFromContext(ctx); tx != nil { 35 | return tx.Exec(ctx, sql, args...) 36 | } 37 | return db.Exec(ctx, sql, args...) 38 | } 39 | 40 | // QueryRow is a wrapper around pgx QueryRow method. 41 | func QueryRow( 42 | ctx context.Context, 43 | db interface { 44 | QueryRow(ctx context.Context, sql string, args ...any) pgx.Row 45 | }, 46 | sql string, 47 | args ...any, 48 | ) pgx.Row { 49 | if tx := txFromContext(ctx); tx != nil { 50 | return tx.QueryRow(ctx, sql, args...) 51 | } 52 | return db.QueryRow(ctx, sql, args...) 53 | } 54 | -------------------------------------------------------------------------------- /runner.go: -------------------------------------------------------------------------------- 1 | package pgxatomic 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/jackc/pgx/v5" 8 | ) 9 | 10 | type txStarter interface { 11 | BeginTx(ctx context.Context, txOptions pgx.TxOptions) (pgx.Tx, error) 12 | } 13 | 14 | // runner starts transaction in Run method by wrapping txFunc using db, 15 | // pgx.Conn and pgxpool.Pool implements db. 16 | type runner struct { 17 | db txStarter 18 | opts pgx.TxOptions 19 | } 20 | 21 | func NewRunner(db txStarter, o pgx.TxOptions) (runner, error) { 22 | if db == nil { 23 | return runner{}, errors.New("pgxatomic: db cannot be nil") 24 | } 25 | 26 | return runner{ 27 | db: db, 28 | opts: o, 29 | }, nil 30 | } 31 | 32 | // Run wraps txFunc in pgx.BeginTxFunc with injected pgx.Tx into context and runs it. 33 | func (r runner) Run(ctx context.Context, txFunc func(ctx context.Context) error) error { 34 | return pgx.BeginTxFunc(ctx, r.db, r.opts, func(tx pgx.Tx) error { 35 | return txFunc(withTx(ctx, tx)) 36 | }) 37 | } 38 | --------------------------------------------------------------------------------