Skip to content

Commit

Permalink
pgx/v5 wrapper with convenience methods
Browse files Browse the repository at this point in the history
  • Loading branch information
vaardan committed Mar 5, 2023
1 parent 5936611 commit 10b2edd
Show file tree
Hide file tree
Showing 9 changed files with 334 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@

# Dependency directories (remove the comment below to include it)
# vendor/
.idea
17 changes: 17 additions & 0 deletions .golangci.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[linters]
disable-all=true
enable = [
"errcheck",
"gosimple",
"govet",
"ineffassign",
"staticcheck",
"typecheck",
"unused",
"forbidigo",
"goimports",
"gosec",
"importas",
"nilnil",
"unconvert",
]
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.PHONY: lint
lint:
# Linting...
golangci-lint run
45 changes: 44 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,45 @@
# pgsq
Wrapper that binds pgx, pgscan, & squirrel together
## About
Wrapper for [pgx/v5](https://github.com/jackc/pgx/) pool.

[scany/v2](https://github.com/georgysavva/scany/) is used to provide _Select_ & _Get_ convenience methods.

All basic (i.e. not raw) methods take _sqlizer_ interface as a query, which is provided by
[squirrel](https://github.com/Masterminds/squirrel) query builder.


## Usage example
Actual data-access methods should take _pgsq.Queryable_ as an argument–this way _CreateEntity_ can be called using a connection
pool or a transaction depending on logical requirements.

```go
package database

import (
"context"
"fmt"

"github.com/vaardan/pgsq"
"github.com/Masterminds/squirrel"
)


// CreateEntity creates new entity with the given name and returns its ID.
func CreateEntity(ctx context.Context, q pgsq.Queryable, name string) (int, error) {
query := squirrel.StatementBuilder.
PlaceholderFormat(squirrel.Dollar).
Insert("entity_table").
Columns("name").
Values(name).
Suffix("returning id")

var id int
err := q.Get(ctx, &id, query)
if err != nil {
return 0, fmt.Errorf("insert entity: %w", err)
}

return id, nil
}

```
58 changes: 58 additions & 0 deletions common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package pgsq

import (
"context"
"fmt"

"github.com/georgysavva/scany/v2/pgxscan"
"github.com/jackc/pgx/v5/pgconn"
)

// Queryable interface containing operations necessary to query database.
// Both Pool and Tx implement it.
type Queryable interface {
// Exec executes the builder query.
Exec(ctx context.Context, query sqlizer) (pgconn.CommandTag, error)

// ExecRaw executes the raw query.
ExecRaw(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)

// Get queries a single row. Returns pgx.ErrNoRows, if there are no rows satisfying the builder query.
Get(ctx context.Context, dst any, sqlizer sqlizer) error

// GetRaw queries a single row. Returns pgx.ErrNoRows, if there are no rows satisfying the raw query.
GetRaw(ctx context.Context, dst any, sql string, args ...any) error

// Select queries multiple rows. Returns nil, if there are no rows satisfying the builder query.
Select(ctx context.Context, dst any, query sqlizer) error

// SelectRaw queries multiple rows. Returns nil, if there are no rows satisfying the raw query.
SelectRaw(ctx context.Context, dst any, sql string, args ...any) error
}

func execFn(ctx context.Context, q execer, query sqlizer) (pgconn.CommandTag, error) {
sql, args, err := query.ToSql()
if err != nil {
return pgconn.CommandTag{}, fmt.Errorf("to sql: %w", err)
}

return q.Exec(ctx, sql, args...)
}

func selectFn(ctx context.Context, q pgxscan.Querier, dst any, query sqlizer) error {
sql, args, err := query.ToSql()
if err != nil {
return fmt.Errorf("to sql: %w", err)
}

return pgxscan.Select(ctx, q, dst, sql, args...)
}

func getFn(ctx context.Context, q pgxscan.Querier, dst any, query sqlizer) error {
sql, args, err := query.ToSql()
if err != nil {
return fmt.Errorf("to sql: %w", err)
}

return pgxscan.Get(ctx, q, dst, sql, args...)
}
17 changes: 17 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module github.com/vaardan/pgsq

go 1.19

require (
github.com/georgysavva/scany/v2 v2.0.0
github.com/jackc/pgx/v5 v5.3.1
)

require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.0 // indirect
golang.org/x/crypto v0.6.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/text v0.7.0 // indirect
)
33 changes: 33 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
github.com/cockroachdb/cockroach-go/v2 v2.2.0 h1:/5znzg5n373N/3ESjHF5SMLxiW4RKB05Ql//KWfeTFs=
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/georgysavva/scany/v2 v2.0.0 h1:RGXqxDv4row7/FYoK8MRXAZXqoWF/NM+NP0q50k3DKU=
github.com/georgysavva/scany/v2 v2.0.0/go.mod h1:sigOdh+0qb/+aOs3TVhehVT10p8qJL7K/Zhyz8vWo38=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
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-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU=
github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
github.com/jackc/puddle/v2 v2.2.0 h1:RdcDk92EJBuBS55nQMMYFXTxwstHug4jkhT5pq8VxPk=
github.com/jackc/puddle/v2 v2.2.0/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
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/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
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.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
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=
81 changes: 81 additions & 0 deletions pool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package pgsq

import (
"context"
"fmt"

"github.com/georgysavva/scany/v2/pgxscan"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgxpool"
)

// Pool contains operations necessary to query with the database.
type Pool interface {
Queryable
BeginTx(ctx context.Context, txOptions *pgx.TxOptions) (Tx, error)
}

type sqlizer interface {
ToSql() (sql string, args []any, err error)
}

type execer interface {
Exec(ctx context.Context, sql string, arguments ...any) (pgconn.CommandTag, error)
}

// NewPool creates new Pool.
func NewPool(pool *pgxpool.Pool) Pool {
return &poolWrapper{pool: pool}
}

// poolWrapper pgx pool wrapper.
type poolWrapper struct {
pool *pgxpool.Pool
}

// BeginTx starts a transaction.
// Commit or Rollback must be called on the returned transaction to finalize the transaction block.
func (p *poolWrapper) BeginTx(ctx context.Context, txOptions *pgx.TxOptions) (Tx, error) {
var txOpts pgx.TxOptions
if txOptions != nil {
txOpts = *txOptions
}

tx, err := p.pool.BeginTx(ctx, txOpts)
if err != nil {
return nil, fmt.Errorf("begin tx: %w", err)
}

return &txWrapper{tx: tx}, nil
}

// Exec executes the builder query.
func (p *poolWrapper) Exec(ctx context.Context, query sqlizer) (pgconn.CommandTag, error) {
return execFn(ctx, p.pool, query)
}

// ExecRaw executes the raw query.
func (p *poolWrapper) ExecRaw(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) {
return p.pool.Exec(ctx, sql, args...)
}

// Get queries a single row. Returns pgx.ErrNoRows, if there are no rows satisfying the builder query.
func (p *poolWrapper) Get(ctx context.Context, dst any, query sqlizer) error {
return getFn(ctx, p.pool, dst, query)
}

// GetRaw queries a single row. Returns pgx.ErrNoRows, if there are no rows satisfying the raw query.
func (p *poolWrapper) GetRaw(ctx context.Context, dst any, sql string, args ...any) error {
return pgxscan.Get(ctx, p.pool, dst, sql, args...)
}

// Select queries multiple rows. Returns nil, if there are no rows satisfying the builder query.
func (p *poolWrapper) Select(ctx context.Context, dst any, query sqlizer) error {
return selectFn(ctx, p.pool, dst, query)
}

// SelectRaw queries multiple rows. Returns nil, if there are no rows satisfying the raw query.
func (p *poolWrapper) SelectRaw(ctx context.Context, dst any, sql string, args ...any) error {
return pgxscan.Select(ctx, p.pool, dst, sql, args...)
}
79 changes: 79 additions & 0 deletions tx.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package pgsq

import (
"context"

"github.com/georgysavva/scany/v2/pgxscan"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)

// Tx transaction interface.
type Tx interface {
Queryable

// Commit commits transaction.
// Will return an error where errors.Is(pgx.ErrTxClosed) is true if the Tx is already closed, but is
// otherwise safe to call multiple times. If the commit fails with a rollback status (e.g. the transaction was already
// in a broken state) then an error where errors.Is(ErrTxCommitRollback) is true will be returned.
Commit(ctx context.Context) error

// Rollback cancels transaction.
// Will return an error where errors.Is(pgx.ErrTxClosed) is true if the Tx is already
// closed, but is otherwise safe to call multiple times. Hence, a defer tx.Rollback() is safe even if tx.Commit() will
// be called first in a non-error condition. Any other failure of a real transaction will result in the connection
// being closed.
Rollback(ctx context.Context) error
}

// Tx transaction wrapper.
type txWrapper struct {
tx pgx.Tx
}

// Commit commits transaction.
// Will return an error where errors.Is(pgx.ErrTxClosed) is true if the Tx is already closed, but is
// otherwise safe to call multiple times. If the commit fails with a rollback status (e.g. the transaction was already
// in a broken state) then an error where errors.Is(ErrTxCommitRollback) is true will be returned.
func (t *txWrapper) Commit(ctx context.Context) error {
return t.tx.Commit(ctx)
}

// Rollback cancels transaction.
// Will return an error where errors.Is(pgx.ErrTxClosed) is true if the Tx is already
// closed, but is otherwise safe to call multiple times. Hence, a defer tx.Rollback() is safe even if tx.Commit() will
// be called first in a non-error condition. Any other failure of a real transaction will result in the connection
// being closed.
func (t *txWrapper) Rollback(ctx context.Context) error {
return t.tx.Rollback(ctx)
}

// Exec executes the builder query.
func (t *txWrapper) Exec(ctx context.Context, query sqlizer) (pgconn.CommandTag, error) {
return execFn(ctx, t.tx, query)
}

// ExecRaw executes the raw query.
func (t *txWrapper) ExecRaw(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) {
return t.tx.Exec(ctx, sql, args...)
}

// Get queries a single row. Returns pgx.ErrNoRows, if there are no rows satisfying the builder query.
func (t *txWrapper) Get(ctx context.Context, dst any, query sqlizer) error {
return getFn(ctx, t.tx, dst, query)
}

// GetRaw queries a single row. Returns pgx.ErrNoRows, if there are no rows satisfying the raw query.
func (t *txWrapper) GetRaw(ctx context.Context, dst any, sql string, args ...any) error {
return pgxscan.Get(ctx, t.tx, dst, sql, args...)
}

// Select queries multiple rows. Returns nil, if there are no rows satisfying the builder query.
func (t *txWrapper) Select(ctx context.Context, dst any, query sqlizer) error {
return selectFn(ctx, t.tx, dst, query)
}

// SelectRaw queries multiple rows. Returns nil, if there are no rows satisfying the raw query.
func (t *txWrapper) SelectRaw(ctx context.Context, dst any, sql string, args ...any) error {
return pgxscan.Select(ctx, t.tx, dst, sql, args...)
}

0 comments on commit 10b2edd

Please sign in to comment.