├── .gitignore ├── .golangci.yml ├── go.mod ├── Makefile ├── options.go ├── LICENSE ├── pig.go ├── ex.go ├── tx.go ├── options_test.go ├── .github └── workflows │ └── test.yml ├── go.sum ├── README.md └── pig_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /vendor 3 | /coverage.out 4 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 5m 3 | tests: false 4 | 5 | linters: 6 | enable-all: true 7 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/otetz/pig 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/georgysavva/scany/v2 v2.1.4 7 | github.com/jackc/pgx/v5 v5.7.5 8 | github.com/pashagolub/pgxmock/v4 v4.8.0 9 | github.com/pkg/errors v0.9.1 10 | ) 11 | 12 | require ( 13 | github.com/jackc/pgpassfile v1.0.0 // indirect 14 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 15 | github.com/jackc/puddle/v2 v2.2.2 // indirect 16 | github.com/stretchr/objx v0.5.2 // indirect 17 | golang.org/x/crypto v0.37.0 // indirect 18 | golang.org/x/sync v0.13.0 // indirect 19 | golang.org/x/text v0.24.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}' 3 | @echo 4 | 5 | .PHONY: lint 6 | lint: ## Run linter 7 | @golangci-lint --exclude-use-default=false run ./... 8 | 9 | runTests: 10 | @go test -v -coverprofile=coverage.out ./... 11 | 12 | .PHONY: test 13 | test: runTests ## Run tests 14 | @if [ -f coverage.out ]; then go tool cover -func=coverage.out && rm coverage.out; fi 15 | 16 | .PHONY: coverage 17 | coverage: runTests ## Show coverage 18 | @if [ -f coverage.out ]; then go tool cover -html=coverage.out && rm coverage.out; fi 19 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package pig 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | // Options query or tx options. 9 | type Options struct { 10 | Context context.Context 11 | TransactionTimeout int64 12 | StatementTimeout int64 13 | } 14 | 15 | // Option func. 16 | type Option func(*Options) 17 | 18 | // Ctx sets query or tx context. 19 | func Ctx(ctx context.Context) Option { 20 | return func(o *Options) { 21 | o.Context = ctx 22 | } 23 | } 24 | 25 | // TransactionTimeout sets transaction timeout (ignored with queries). 26 | func TransactionTimeout(d time.Duration) Option { 27 | return func(o *Options) { 28 | o.TransactionTimeout = d.Milliseconds() 29 | } 30 | } 31 | 32 | // StatementTimeout sets transaction statement timeout (ignored with queries). 33 | func StatementTimeout(d time.Duration) Option { 34 | return func(o *Options) { 35 | o.StatementTimeout = d.Milliseconds() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Alexey Popov 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 | -------------------------------------------------------------------------------- /pig.go: -------------------------------------------------------------------------------- 1 | // Package pig – simple pgx wrapper to execute and scan query results. 2 | package pig 3 | 4 | import ( 5 | "context" 6 | 7 | "github.com/jackc/pgx/v5" 8 | "github.com/jackc/pgx/v5/pgconn" 9 | ) 10 | 11 | // Conn connection interface. 12 | type Conn interface { 13 | BeginTx(context.Context, pgx.TxOptions) (pgx.Tx, error) 14 | Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) 15 | Query(context.Context, string, ...interface{}) (pgx.Rows, error) 16 | } 17 | 18 | // Handler to execute transaction. 19 | type Handler func(*Ex) error 20 | 21 | // Pig pgx wrapper. 22 | type Pig struct { 23 | conn Conn 24 | } 25 | 26 | // Conn returns pgx connection. 27 | func (p *Pig) Conn() Conn { 28 | return p.conn 29 | } 30 | 31 | // Query returns new query executor. 32 | func (p *Pig) Query(options ...Option) *Ex { 33 | return &Ex{ 34 | ex: p.conn, 35 | options: p.options(options...), 36 | } 37 | } 38 | 39 | // Tx returns new transaction. 40 | func (p *Pig) Tx(options ...Option) *Tx { 41 | return &Tx{ 42 | conn: p.conn, 43 | options: p.options(options...), 44 | } 45 | } 46 | 47 | func (p *Pig) options(options ...Option) Options { 48 | var o Options 49 | for _, opt := range options { 50 | opt(&o) 51 | } 52 | 53 | if o.Context == nil { 54 | o.Context = context.Background() 55 | } 56 | 57 | return o 58 | } 59 | 60 | // New returns new pig instance. 61 | func New(conn Conn) *Pig { 62 | return &Pig{ 63 | conn: conn, 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /ex.go: -------------------------------------------------------------------------------- 1 | package pig 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/georgysavva/scany/v2/pgxscan" 7 | "github.com/jackc/pgx/v5" 8 | "github.com/jackc/pgx/v5/pgconn" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | type executable interface { 13 | Exec(context.Context, string, ...any) (pgconn.CommandTag, error) 14 | Query(context.Context, string, ...any) (pgx.Rows, error) 15 | } 16 | 17 | // Ex to execute queries. 18 | type Ex struct { 19 | ex executable 20 | options Options 21 | } 22 | 23 | // Exec query and return affected rows. 24 | func (e *Ex) Exec(sql string, args ...interface{}) (int64, error) { 25 | t, err := e.ex.Exec(e.options.Context, sql, args...) 26 | if err != nil { 27 | return 0, errors.Wrap(err, "pig: execute query") 28 | } 29 | 30 | return t.RowsAffected(), nil 31 | } 32 | 33 | // Get single record. 34 | func (e *Ex) Get(dst interface{}, sql string, args ...interface{}) error { 35 | rows, err := e.ex.Query(e.options.Context, sql, args...) 36 | if err != nil { 37 | return errors.Wrap(err, "pig: get one result row") 38 | } 39 | 40 | err = pgxscan.ScanOne(dst, rows) 41 | 42 | return errors.WithStack(err) 43 | } 44 | 45 | // Select multiple records. 46 | func (e *Ex) Select(dst interface{}, sql string, args ...interface{}) error { 47 | rows, err := e.ex.Query(e.options.Context, sql, args...) 48 | if err != nil { 49 | return errors.Wrap(err, "pig: select multiple result row") 50 | } 51 | 52 | err = pgxscan.ScanAll(dst, rows) 53 | 54 | return errors.WithStack(err) 55 | } 56 | -------------------------------------------------------------------------------- /tx.go: -------------------------------------------------------------------------------- 1 | package pig 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/jackc/pgx/v5" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | const ( 11 | transactionTimeoutQuery = "SET local idle_in_transaction_session_timeout = $1" 12 | statementTimeoutQuery = "SET local statement_timeout = $1" 13 | ) 14 | 15 | // Tx transaction. 16 | type Tx struct { 17 | conn Conn 18 | options Options 19 | } 20 | 21 | // Exec to execute transaction. 22 | func (tx *Tx) Exec(handler Handler) error { 23 | txx, err := tx.conn.BeginTx(tx.options.Context, pgx.TxOptions{}) 24 | if err != nil { 25 | return errors.Wrap(err, "pig: begin transaction") 26 | } 27 | 28 | defer func() { 29 | switch err { 30 | case nil: 31 | err = txx.Commit(context.Background()) 32 | if err != nil { 33 | _ = txx.Rollback(context.Background()) 34 | } 35 | default: 36 | _ = txx.Rollback(context.Background()) 37 | } 38 | }() 39 | 40 | if tx.options.TransactionTimeout > 0 { 41 | if _, err = txx.Exec(tx.options.Context, transactionTimeoutQuery, tx.options.TransactionTimeout); err != nil { 42 | return errors.Wrap(err, "pig: set transaction timeout") 43 | } 44 | } 45 | 46 | if tx.options.StatementTimeout > 0 { 47 | if _, err = txx.Exec(tx.options.Context, statementTimeoutQuery, tx.options.StatementTimeout); err != nil { 48 | return errors.Wrap(err, "pig: set statement timeout") 49 | } 50 | } 51 | 52 | err = handler(&Ex{ 53 | ex: txx, 54 | options: tx.options, 55 | }) 56 | 57 | return errors.WithStack(err) 58 | } 59 | -------------------------------------------------------------------------------- /options_test.go: -------------------------------------------------------------------------------- 1 | package pig_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/otetz/pig" 9 | ) 10 | 11 | type key int 12 | 13 | const expectedContextKey key = 123 14 | 15 | func TestCtx(t *testing.T) { 16 | t.Parallel() 17 | 18 | expectedValue := 234 19 | 20 | ctx := context.Background() 21 | ctx = context.WithValue(ctx, expectedContextKey, expectedValue) 22 | 23 | var o pig.Options 24 | if o.Context != nil { 25 | t.Error(`should be nil`) 26 | } 27 | 28 | pig.Ctx(ctx)(&o) 29 | 30 | v := o.Context.Value(expectedContextKey) 31 | if v == nil { 32 | t.Fatal(`should not be nil`) 33 | } 34 | 35 | if v.(int) != expectedValue { 36 | t.Errorf(`should be %d, %d given`, expectedValue, v.(int)) 37 | } 38 | } 39 | 40 | func TestTransactionTimeout(t *testing.T) { 41 | t.Parallel() 42 | 43 | var o pig.Options 44 | if o.TransactionTimeout != 0 { 45 | t.Errorf(`should be %d, %d given`, 0, o.TransactionTimeout) 46 | } 47 | 48 | pig.TransactionTimeout(time.Second)(&o) 49 | 50 | if o.TransactionTimeout != time.Second.Milliseconds() { 51 | t.Errorf(`should be %d, %d given`, time.Second.Milliseconds(), o.TransactionTimeout) 52 | } 53 | } 54 | 55 | func TestStatementTimeout(t *testing.T) { 56 | t.Parallel() 57 | 58 | var o pig.Options 59 | if o.StatementTimeout != 0 { 60 | t.Errorf(`should be %d, %d given`, 0, o.StatementTimeout) 61 | } 62 | 63 | pig.StatementTimeout(time.Second)(&o) 64 | 65 | if o.StatementTimeout != time.Second.Milliseconds() { 66 | t.Errorf(`should be %d, %d given`, time.Second.Milliseconds(), o.StatementTimeout) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Test 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | branches: 8 | - main 9 | pull_request: 10 | jobs: 11 | test: 12 | name: Test 13 | runs-on: ubuntu-latest 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | go: 18 | - 1.14 19 | - 1.15 20 | - 1.x # Latest version 21 | steps: 22 | - name: Check out 23 | uses: actions/checkout@v2 24 | with: 25 | path: ${{ github.workspace }}/go/src/github.com/alexeyco/pig 26 | - name: Install Go 27 | uses: actions/setup-go@v2 28 | with: 29 | go-version: ${{ matrix.go-version }} 30 | - name: Show Go version 31 | run: go version 32 | - name: Install dependencies 33 | working-directory: ${{ github.workspace }}/go/src/github.com/alexeyco/pig 34 | run: go mod vendor 35 | - name: Lint source code 36 | uses: golangci/golangci-lint-action@v2 37 | with: 38 | working-directory: ${{ github.workspace }}/go/src/github.com/alexeyco/pig 39 | version: v1.38.0 40 | - name: Run tests 41 | working-directory: ${{ github.workspace }}/go/src/github.com/alexeyco/pig 42 | run: go test -race -covermode atomic -coverprofile=coverage.out . 43 | - name: Install goveralls 44 | env: 45 | GO111MODULE: off 46 | run: go get github.com/mattn/goveralls 47 | - name: Upload coverage 48 | env: 49 | COVERALLS_TOKEN: ${{ github.token }} 50 | GIT_BRANCH: ${{ github.head_ref }} 51 | working-directory: ${{ github.workspace }}/go/src/github.com/alexeyco/pig 52 | run: goveralls -coverprofile=coverage.out -service=github 53 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cockroachdb/cockroach-go/v2 v2.2.0 h1:/5znzg5n373N/3ESjHF5SMLxiW4RKB05Ql//KWfeTFs= 2 | github.com/cockroachdb/cockroach-go/v2 v2.2.0/go.mod h1:u3MiKYGupPPjkn3ozknpMUpxPaNLTFWAya419/zv6eI= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/georgysavva/scany/v2 v2.1.4 h1:nrzHEJ4oQVRoiKmocRqA1IyGOmM/GQOEsg9UjMR5Ip4= 7 | github.com/georgysavva/scany/v2 v2.1.4/go.mod h1:fqp9yHZzM/PFVa3/rYEC57VmDx+KDch0LoqrJzkvtos= 8 | github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= 9 | github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= 10 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 11 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 12 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= 13 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 14 | github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= 15 | github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= 16 | github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= 17 | github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 18 | github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E= 19 | github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 20 | github.com/pashagolub/pgxmock/v4 v4.8.0 h1:RBtNUZXNG/ZwyOT7sJdSEx9RlAw19sgVPlnmEdlpT08= 21 | github.com/pashagolub/pgxmock/v4 v4.8.0/go.mod h1:9L57pC193h2aKRHVyiiE817avasIPZnPwPlw3JczWvM= 22 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 23 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 24 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 25 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 26 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 27 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 28 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 29 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 30 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 31 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 32 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 33 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 34 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 35 | golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= 36 | golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 37 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 38 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 39 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 40 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 41 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 42 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 43 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 44 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pig 2 | 3 | [![Build](https://github.com/alexeyco/pig/actions/workflows/test.yml/badge.svg)](https://github.com/alexeyco/pig/actions/workflows/test.yml) 4 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/alexeyco/pig)](https://pkg.go.dev/github.com/alexeyco/pig) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/alexeyco/pig)](https://goreportcard.com/report/github.com/alexeyco/pig) 6 | [![Coverage Status](https://coveralls.io/repos/github/alexeyco/pig/badge.svg?branch=main)](https://coveralls.io/github/alexeyco/pig?branch=main) 7 | 8 | Simple [pgx](https://github.com/jackc/pgx) wrapper to execute and [scan](https://github.com/alexeyco/pig) query 9 | results. 10 | 11 | ## Features 12 | 13 | * All-in-one tool; 14 | * Simple transactions management: 15 | * You can set `idle_in_transaction_session_timeout` local 16 | option ([read more](https://www.postgresql.org/docs/9.6/runtime-config-client.html)), 17 | * You can set `statement_timeout` local 18 | option ([read more](https://www.postgresql.org/docs/9.6/runtime-config-client.html)). 19 | 20 | ## Usage 21 | 22 | ### Execute query 23 | 24 | ```go 25 | package main 26 | 27 | import ( 28 | "context" 29 | "log" 30 | 31 | "github.com/alexeyco/pig" 32 | "github.com/jackc/pgx/v5" 33 | ) 34 | 35 | func main() { 36 | conn, err := pgx.Connect(context.Background(), "") 37 | if err != nil { 38 | log.Fatalln(err) 39 | } 40 | 41 | p := pig.New(conn) 42 | 43 | affectedRows, err := p.Query().Exec("DELETE FROM things WHERE id = $1", 123) 44 | if err != nil { 45 | log.Fatalln(err) 46 | } 47 | 48 | log.Println("affected", affectedRows, "rows") 49 | } 50 | ``` 51 | 52 | ### Get single entity 53 | 54 | ```go 55 | package main 56 | 57 | import ( 58 | "context" 59 | "log" 60 | 61 | "github.com/alexeyco/pig" 62 | "github.com/jackc/pgx/v5" 63 | ) 64 | 65 | func main() { 66 | conn, err := pgx.Connect(context.Background(), "") 67 | if err != nil { 68 | log.Fatalln(err) 69 | } 70 | 71 | p := pig.New(conn) 72 | 73 | var cnt int64 74 | err = p.Query().Get(&cnt, "SELECT count(*) FROM things") 75 | if err != nil { 76 | log.Fatalln(err) 77 | } 78 | 79 | log.Println(cnt, "things found") 80 | } 81 | ``` 82 | 83 | ### Select multiple entities 84 | 85 | ```go 86 | package main 87 | 88 | import ( 89 | "context" 90 | "log" 91 | 92 | "github.com/alexeyco/pig" 93 | "github.com/jackc/pgx/v5" 94 | ) 95 | 96 | type Thing struct { 97 | ID int64 `db:"id"` 98 | Name string `db:"name"` 99 | Quantity int64 `db:"quantity"` 100 | } 101 | 102 | func main() { 103 | conn, err := pgx.Connect(context.Background(), "") 104 | if err != nil { 105 | log.Fatalln(err) 106 | } 107 | 108 | p := pig.New(conn) 109 | 110 | var things []Thing 111 | err = p.Query().Select(&things, "SELECT * FROM things") 112 | if err != nil { 113 | log.Fatalln(err) 114 | } 115 | 116 | log.Println(things) 117 | } 118 | ``` 119 | 120 | ### Make transactions 121 | 122 | ```go 123 | package main 124 | 125 | import ( 126 | "context" 127 | "log" 128 | "time" 129 | 130 | "github.com/alexeyco/pig" 131 | "github.com/jackc/pgx/v5" 132 | ) 133 | 134 | func main() { 135 | conn, err := pgx.Connect(context.Background(), "") 136 | if err != nil { 137 | log.Fatalln(err) 138 | } 139 | 140 | p := pig.New(conn) 141 | 142 | var affectedRows int64 143 | err = p.Tx(pig.TransactionTimeout(time.Second)). 144 | Exec(func(ex *pig.Ex) error { 145 | affectedRows, err = p.Query().Exec("DELETE FROM things WHERE id = $1", 123) 146 | if err != nil { 147 | return err 148 | } 149 | 150 | return nil 151 | }) 152 | if err != nil { 153 | log.Fatalln(err) 154 | } 155 | 156 | log.Println("affected", affectedRows, "rows") 157 | } 158 | ``` 159 | 160 | ## License 161 | 162 | ``` 163 | MIT License 164 | 165 | Copyright (c) 2021 Alexey Popov 166 | 167 | Permission is hereby granted, free of charge, to any person obtaining a copy 168 | of this software and associated documentation files (the "Software"), to deal 169 | in the Software without restriction, including without limitation the rights 170 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 171 | copies of the Software, and to permit persons to whom the Software is 172 | furnished to do so, subject to the following conditions: 173 | 174 | The above copyright notice and this permission notice shall be included in all 175 | copies or substantial portions of the Software. 176 | 177 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 178 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 179 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 180 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 181 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 182 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 183 | SOFTWARE. 184 | ``` 185 | -------------------------------------------------------------------------------- /pig_test.go: -------------------------------------------------------------------------------- 1 | package pig_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "reflect" 8 | "strings" 9 | "testing" 10 | "time" 11 | 12 | "github.com/jackc/pgx/v5" 13 | "github.com/pashagolub/pgxmock/v4" 14 | "github.com/pkg/errors" 15 | 16 | "github.com/otetz/pig" 17 | ) 18 | 19 | func connect(t *testing.T) pgxmock.PgxConnIface { 20 | t.Helper() 21 | 22 | conn, err := pgxmock.NewConn(pgxmock.QueryMatcherOption(pgxmock.QueryMatcherEqual)) 23 | if err != nil { 24 | t.Fatalf(`error should be nil, "%v" given`, err) 25 | } 26 | 27 | conn.MatchExpectationsInOrder(true) 28 | 29 | return conn 30 | } 31 | 32 | type thing struct { 33 | ID int64 `db:"id"` 34 | Name string `db:"name"` 35 | Quantity int64 `db:"quantity"` 36 | } 37 | 38 | func (t thing) String() string { 39 | return fmt.Sprintf(`{ID: %d, Name: "%s", Quantity: %d}`, t.ID, t.Name, t.Quantity) 40 | } 41 | 42 | func (t thing) isZero() bool { 43 | return t.ID == 0 && t.Name == "" && t.Quantity == 0 44 | } 45 | 46 | type things []thing 47 | 48 | func (t things) String() string { 49 | parts := make([]string, len(t)) 50 | for n, th := range t { 51 | parts[n] = th.String() 52 | } 53 | 54 | return strings.Join(parts, ", ") 55 | } 56 | 57 | var errExpected = errors.New("i am error") 58 | 59 | func TestPig_Conn(t *testing.T) { 60 | t.Parallel() 61 | 62 | conn := connect(t) 63 | defer func() { _ = conn.Close(context.Background()) }() 64 | 65 | if !reflect.DeepEqual(pig.New(conn).Conn(), conn) { 66 | t.Fatal(`should be equal`) 67 | } 68 | } 69 | 70 | func TestPig_Query(t *testing.T) { 71 | t.Parallel() 72 | 73 | t.Run("ExecOk", func(t *testing.T) { 74 | t.Parallel() 75 | 76 | conn := connect(t) 77 | defer func() { _ = conn.Close(context.Background()) }() 78 | 79 | conn.ExpectExec("DELETE FROM things WHERE id = $1"). 80 | WithArgs(123). 81 | WillReturnResult(pgxmock.NewResult("DELETE", 1)) 82 | 83 | rowsAffected, err := pig.New(conn). 84 | Query(). 85 | Exec("DELETE FROM things WHERE id = $1", 123) 86 | if err != nil { 87 | t.Fatalf(`should be nil, "%v" given`, err) 88 | } 89 | 90 | if rowsAffected != 1 { 91 | t.Errorf(`should be %d, %d given`, 1, rowsAffected) 92 | } 93 | 94 | if err = conn.ExpectationsWereMet(); err != nil { 95 | t.Errorf(`there were unfulfilled expectations: %v`, err) 96 | } 97 | }) 98 | 99 | t.Run("ExecFailed", func(t *testing.T) { 100 | t.Parallel() 101 | 102 | conn := connect(t) 103 | defer func() { _ = conn.Close(context.Background()) }() 104 | 105 | conn.ExpectExec("DELETE FROM things WHERE id = $1"). 106 | WithArgs(123). 107 | WillReturnError(errExpected) 108 | 109 | rowsAffected, err := pig.New(conn). 110 | Query(). 111 | Exec("DELETE FROM things WHERE id = $1", 123) 112 | 113 | if err == nil { 114 | t.Fatal(`should not be nil`) 115 | } 116 | 117 | if rowsAffected != 0 { 118 | t.Errorf(`should be %d, %d given`, 0, rowsAffected) 119 | } 120 | 121 | if err = conn.ExpectationsWereMet(); err != nil { 122 | t.Errorf(`there were unfulfilled expectations: %v`, err) 123 | } 124 | }) 125 | 126 | t.Run("GetOk", func(t *testing.T) { 127 | t.Parallel() 128 | 129 | conn := connect(t) 130 | defer func() { _ = conn.Close(context.Background()) }() 131 | 132 | rows := conn.NewRows([]string{"id", "name", "quantity"}). 133 | AddRow(int64(123), "Some thing", int64(456)) 134 | 135 | conn.ExpectQuery("SELECT * FROM things WHERE id = $1"). 136 | WithArgs(123). 137 | WillReturnRows(rows) 138 | 139 | var actual thing 140 | err := pig.New(conn). 141 | Query(). 142 | Get(&actual, "SELECT * FROM things WHERE id = $1", 123) 143 | if err != nil { 144 | t.Fatalf(`should be nil, "%v" given`, err) 145 | } 146 | 147 | if err = conn.ExpectationsWereMet(); err != nil { 148 | t.Errorf(`there were unfulfilled expectations: %v`, err) 149 | } 150 | 151 | expected := thing{ 152 | ID: 123, 153 | Name: "Some thing", 154 | Quantity: 456, 155 | } 156 | 157 | if !reflect.DeepEqual(expected, actual) { 158 | t.Errorf(`result should be %s, %s given`, expected, actual) 159 | } 160 | }) 161 | 162 | t.Run("GetFailed", func(t *testing.T) { 163 | t.Parallel() 164 | 165 | conn := connect(t) 166 | defer func() { _ = conn.Close(context.Background()) }() 167 | 168 | conn.ExpectQuery("SELECT * FROM things WHERE id = $1"). 169 | WithArgs(123). 170 | WillReturnError(errExpected) 171 | 172 | var actual thing 173 | err := pig.New(conn). 174 | Query(). 175 | Get(&actual, "SELECT * FROM things WHERE id = $1", 123) 176 | if err == nil { 177 | t.Fatal(`should not be nil`) 178 | } 179 | 180 | if err = conn.ExpectationsWereMet(); err != nil { 181 | t.Errorf(`there were unfulfilled expectations: %v`, err) 182 | } 183 | 184 | if !actual.isZero() { 185 | t.Errorf(`result should be empty, %s given`, actual) 186 | } 187 | }) 188 | 189 | t.Run("SelectOk", func(t *testing.T) { 190 | t.Parallel() 191 | 192 | conn := connect(t) 193 | defer func() { _ = conn.Close(context.Background()) }() 194 | 195 | rows := conn.NewRows([]string{"id", "name", "quantity"}). 196 | AddRow(int64(123), "Some thing1", int64(456)). 197 | AddRow(int64(789), "Some thing2", int64(123)) 198 | 199 | conn.ExpectQuery("SELECT * FROM things WHERE id = $1"). 200 | WithArgs(123). 201 | WillReturnRows(rows) 202 | 203 | var actual things 204 | err := pig.New(conn). 205 | Query(). 206 | Select(&actual, "SELECT * FROM things WHERE id = $1", 123) 207 | if err != nil { 208 | t.Fatalf(`should be nil, "%v" given`, err) 209 | } 210 | 211 | if err = conn.ExpectationsWereMet(); err != nil { 212 | t.Errorf(`there were unfulfilled expectations: %v`, err) 213 | } 214 | 215 | expected := things{ 216 | { 217 | ID: 123, 218 | Name: "Some thing1", 219 | Quantity: 456, 220 | }, 221 | { 222 | ID: 789, 223 | Name: "Some thing2", 224 | Quantity: 123, 225 | }, 226 | } 227 | 228 | if !reflect.DeepEqual(expected, actual) { 229 | t.Errorf(`result should be %s, %s given`, expected, actual) 230 | } 231 | }) 232 | 233 | t.Run("SelectFailed", func(t *testing.T) { 234 | t.Parallel() 235 | 236 | conn := connect(t) 237 | defer func() { _ = conn.Close(context.Background()) }() 238 | 239 | conn.ExpectQuery("SELECT * FROM things WHERE id = $1"). 240 | WithArgs(123). 241 | WillReturnError(errExpected) 242 | 243 | var actual things 244 | err := pig.New(conn). 245 | Query(). 246 | Select(&actual, "SELECT * FROM things WHERE id = $1", 123) 247 | if err == nil { 248 | t.Fatal(`should not be nil`) 249 | } 250 | 251 | if err = conn.ExpectationsWereMet(); err != nil { 252 | t.Errorf(`there were unfulfilled expectations: %v`, err) 253 | } 254 | 255 | if len(actual) != 0 { 256 | t.Errorf(`result should be empty, %s given`, actual) 257 | } 258 | }) 259 | } 260 | 261 | func TestPig_Tx(t *testing.T) { 262 | t.Parallel() 263 | 264 | t.Run("Ok", func(t *testing.T) { 265 | t.Parallel() 266 | 267 | conn := connect(t) 268 | defer func() { _ = conn.Close(context.Background()) }() 269 | 270 | conn.ExpectBegin() 271 | conn.ExpectExec("DELETE FROM things WHERE id = $1"). 272 | WithArgs(123). 273 | WillReturnResult(pgxmock.NewResult("DELETE", 1)) 274 | conn.ExpectRollback() 275 | 276 | err := pig.New(conn). 277 | Tx(). 278 | Exec(func(ex *pig.Ex) error { 279 | _, err := ex.Exec("DELETE FROM things WHERE id = $1", 123) 280 | 281 | return err 282 | }) 283 | if err != nil { 284 | t.Fatalf(`should be nil, "%v" given`, err) 285 | } 286 | 287 | if err = conn.ExpectationsWereMet(); err != nil { 288 | t.Errorf(`there were unfulfilled expectations: %v`, err) 289 | } 290 | }) 291 | 292 | t.Run("Failed", func(t *testing.T) { 293 | t.Parallel() 294 | 295 | conn := connect(t) 296 | defer func() { _ = conn.Close(context.Background()) }() 297 | 298 | conn.ExpectBegin() 299 | conn.ExpectExec("DELETE FROM things WHERE id = $1"). 300 | WithArgs(123). 301 | WillReturnError(errExpected) 302 | conn.ExpectRollback() 303 | 304 | err := pig.New(conn). 305 | Tx(). 306 | Exec(func(ex *pig.Ex) error { 307 | _, err := ex.Exec("DELETE FROM things WHERE id = $1", 123) 308 | 309 | return err 310 | }) 311 | if err == nil { 312 | t.Fatal(`should not be nil`) 313 | } 314 | 315 | if !errors.Is(err, errExpected) { 316 | t.Errorf(`should be "%v", "%v" given`, errExpected, err) 317 | } 318 | }) 319 | 320 | t.Run("StatementTimeoutOk", func(t *testing.T) { 321 | t.Parallel() 322 | 323 | conn := connect(t) 324 | defer func() { _ = conn.Close(context.Background()) }() 325 | 326 | conn.ExpectBegin() 327 | conn.ExpectExec(`SET local statement_timeout = $1`). 328 | WithArgs(int64(1000)). 329 | WillReturnResult(pgxmock.NewResult("SET", 1)) 330 | conn.ExpectExec("DELETE FROM things WHERE id = $1"). 331 | WithArgs(123). 332 | WillReturnResult(pgxmock.NewResult("DELETE", 1)) 333 | conn.ExpectRollback() 334 | 335 | err := pig.New(conn). 336 | Tx(pig.StatementTimeout(time.Second)). 337 | Exec(func(ex *pig.Ex) error { 338 | _, err := ex.Exec("DELETE FROM things WHERE id = $1", 123) 339 | 340 | return err 341 | }) 342 | if err != nil { 343 | t.Fatalf(`should be nil, "%v" given`, err) 344 | } 345 | 346 | if err = conn.ExpectationsWereMet(); err != nil { 347 | t.Errorf(`there were unfulfilled expectations: %v`, err) 348 | } 349 | }) 350 | 351 | t.Run("StatementTimeoutFailed", func(t *testing.T) { 352 | t.Parallel() 353 | 354 | conn := connect(t) 355 | defer func() { _ = conn.Close(context.Background()) }() 356 | 357 | conn.ExpectBegin() 358 | conn.ExpectExec(`SET local statement_timeout = $1`). 359 | WithArgs(int64(1000)). 360 | WillReturnError(errExpected) 361 | conn.ExpectRollback() 362 | conn.ExpectRollback() 363 | 364 | err := pig.New(conn). 365 | Tx(pig.StatementTimeout(time.Second)). 366 | Exec(func(ex *pig.Ex) error { 367 | _, err := ex.Exec("DELETE FROM things WHERE id = $1", 123) 368 | 369 | return err 370 | }) 371 | if err == nil { 372 | t.Fatal(`should not be nil`) 373 | } 374 | 375 | if !errors.Is(err, errExpected) { 376 | t.Errorf(`should be "%v", "%v" given`, errExpected, err) 377 | } 378 | }) 379 | 380 | t.Run("TransactionTimeoutOk", func(t *testing.T) { 381 | t.Parallel() 382 | 383 | conn := connect(t) 384 | defer func() { _ = conn.Close(context.Background()) }() 385 | 386 | conn.ExpectBegin() 387 | conn.ExpectExec(`SET local idle_in_transaction_session_timeout = $1`). 388 | WithArgs(int64(1000)). 389 | WillReturnResult(pgxmock.NewResult("SET", 1)) 390 | conn.ExpectExec("DELETE FROM things WHERE id = $1"). 391 | WithArgs(123). 392 | WillReturnResult(pgxmock.NewResult("DELETE", 1)) 393 | conn.ExpectRollback() 394 | 395 | err := pig.New(conn). 396 | Tx(pig.TransactionTimeout(time.Second)). 397 | Exec(func(ex *pig.Ex) error { 398 | _, err := ex.Exec("DELETE FROM things WHERE id = $1", 123) 399 | 400 | return err 401 | }) 402 | if err != nil { 403 | t.Fatalf(`should be nil, "%v" given`, err) 404 | } 405 | 406 | if err = conn.ExpectationsWereMet(); err != nil { 407 | t.Errorf(`there were unfulfilled expectations: %v`, err) 408 | } 409 | }) 410 | 411 | t.Run("TransactionTimeoutFailed", func(t *testing.T) { 412 | t.Parallel() 413 | 414 | conn := connect(t) 415 | defer func() { _ = conn.Close(context.Background()) }() 416 | 417 | conn.ExpectBegin() 418 | conn.ExpectExec(`SET local idle_in_transaction_session_timeout = $1`). 419 | WithArgs(int64(1000)). 420 | WillReturnError(errExpected) 421 | conn.ExpectRollback() 422 | conn.ExpectRollback() 423 | 424 | err := pig.New(conn). 425 | Tx(pig.TransactionTimeout(time.Second)). 426 | Exec(func(ex *pig.Ex) error { 427 | _, err := ex.Exec("DELETE FROM things WHERE id = $1", 123) 428 | 429 | return err 430 | }) 431 | if err == nil { 432 | t.Fatal(`should not be nil`) 433 | } 434 | 435 | if !errors.Is(err, errExpected) { 436 | t.Errorf(`should be "%v", "%v" given`, errExpected, err) 437 | } 438 | }) 439 | } 440 | 441 | func ExamplePig_Query() { 442 | conn, err := pgx.Connect(context.Background(), "") 443 | if err != nil { 444 | log.Fatalln(err) 445 | } 446 | 447 | p := pig.New(conn) 448 | 449 | // Execute query 450 | affectedRows, err := p.Query().Exec("DELETE FROM things WHERE id = $1", 123) 451 | if err != nil { 452 | log.Fatalln(err) 453 | } 454 | 455 | log.Println("affected", affectedRows, "rows") 456 | 457 | // Get single record from database 458 | var cnt int64 459 | err = p.Query().Get(&cnt, "SELECT count(*) FROM things") 460 | if err != nil { 461 | log.Fatalln(err) 462 | } 463 | 464 | type Thing struct { 465 | ID int64 `db:"id"` 466 | Name string `db:"name"` 467 | Quantity int64 `db:"quantity"` 468 | } 469 | 470 | // Select multiple records 471 | var things []Thing 472 | err = p.Query().Select(&things, "SELECT * FROM things") 473 | if err != nil { 474 | log.Fatalln(err) 475 | } 476 | 477 | log.Println(things) 478 | } 479 | 480 | func ExamplePig_Tx() { 481 | conn, err := pgx.Connect(context.Background(), "") 482 | if err != nil { 483 | log.Fatalln(err) 484 | } 485 | 486 | p := pig.New(conn) 487 | 488 | var affectedRows int64 489 | err = p.Tx().Exec(func(ex *pig.Ex) error { 490 | affectedRows, err = p.Query().Exec("DELETE FROM things WHERE id = $1", 123) 491 | if err != nil { 492 | return err 493 | } 494 | 495 | return nil 496 | }) 497 | if err != nil { 498 | log.Fatalln(err) 499 | } 500 | 501 | log.Println("affected", affectedRows, "rows") 502 | } 503 | --------------------------------------------------------------------------------