Documentation
¶
Overview ¶
Package sql wraps a PostgreSQL connection with context-scoped transactions, Datadog tracing and typed errors.
A connection is built from a Config and used through the Connection interface:
conn, err := sql.New(sql.Config{
Host: "localhost", Port: "5432",
User: "postgres", Password: pw,
Database: "orders", SSLMode: "disable",
MaxOpenConn: 10, MaxIdleConn: 5,
})
var o Order
err = conn.Get(ctx, &o, "SELECT id, total FROM orders WHERE id = $1", id)
Scanning is sqlx over the pgx stdlib driver: columns map to struct fields by lowercased field name, or by an explicit `db` tag, and Select fills a slice.
Transactions travel in the context rather than in a separate handle. TransactionContext begins one, puts it in the context it passes to fn, and commits when fn returns nil or rolls back when it returns an error:
err := conn.TransactionContext(ctx, func(ctx context.Context) error {
if _, err := conn.Exec(ctx, debit, from); err != nil {
return err
}
_, err := conn.Exec(ctx, credit, to)
return err
})
The point of that shape is that repository code keeps calling the same Connection methods either way: Get, Select and Exec look for a transaction in the context and use it when there is one, or go to the pool when there is not. Nothing has to be threaded through a separate parameter.
Driver errors are translated. Known PostgreSQL SQLSTATE codes become the sentinel errors declared in this package, so callers match with errors.Is instead of comparing strings:
if errors.Is(err, sql.ErrUniqueViolation) { ... }
Anything unmapped becomes ErrGeneral. Note that the translation keeps the message but not the *pgconn.PgError itself, so details such as the constraint name are not reachable from the returned error.
Config carries a password. Its String method redacts it, but the dsn built for the driver does not - never log the value returned by an internal helper, and prefer %v on the Config, which goes through String.
Every method on the connection opens a Datadog span tagged with the query.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrConn represents an error while connecting to the database. ErrConn = errors.New("error while connecting to database") // ErrSyntax represents a syntax error with code 42601, in which the query is malformed. ErrSyntax = errors.New("syntax Error 42601") // ErrUniqueViolation represents a unique violation error with code 23505, in which the value // already exists in the database and can't be duplicated. ErrUniqueViolation = errors.New("unique violation 23505") // ErrForeignKeyViolation represents a foreign key violation error with code 23503, in which the FK does // not exist. ErrForeignKeyViolation = errors.New("foreign key violation 23503") // ErrNoParameter represents a no parameter error with code 42P02, that may happen when a parameter is missing // for example: 'INSERT INTO deals (affiliate_id, value) VALUES ($1, $2)', where $1 is missing. ErrNoParameter = errors.New("no parameter 42P02") // ErrGeneral represents any err that has not been mapped yet, the err then should be asserted to // the pgconn.PgError type. ErrGeneral = errors.New("something went wrong") )
Errors that can be returned by the sql package.
Functions ¶
func WithTransaction ¶
func WithTransaction(ctx context.Context, transaction Transaction) context.Context
WithTransaction returns a context carrying transaction, so that later calls to Get, Select and Exec on a Connection run inside it.
TransactionContext does this for you. Call it directly only when managing a transaction by hand.
Types ¶
type Config ¶
type Config struct {
Host string
Port string
User string
Password string
Database string
SSLMode string
MaxOpenConn int
ConnMaxIdleTime time.Duration
ConnMaxLifetime time.Duration
MaxIdleConn int
}
Config describes how to reach a PostgreSQL server and how to size the pool.
The zero value of the pool fields means the database/sql defaults: unlimited open connections, and no maximum idle time or lifetime.
Password is redacted by String, so log the Config itself rather than any string built from its fields.
type Connection ¶
type Connection interface {
Queryer
Execer
Begin(ctx context.Context) (Transaction, error)
TransactionContext(ctx context.Context, fn func(ctx context.Context) error) error
Ping() error
Close() error
}
Connection is a PostgreSQL connection pool.
Get, Select and Exec look for a transaction in the context and use it when present, so the same call works inside and outside TransactionContext.
Ping and Close take no context and cannot be cancelled.
func New ¶
func New(cfg Config) (Connection, error)
New opens a connection pool and verifies it with a ping.
It returns an error wrapping ErrConn when the server cannot be reached.
Example ¶
package main
import (
"context"
"fmt"
dbsql "github.com/diegodesousas/go-devkit/pkg/database/sql"
)
type account struct {
ID int `db:"id"`
Owner string `db:"owner"`
Balance int `db:"balance"`
}
// connect is the shared set-up for the examples below. Every one of them needs a
// live PostgreSQL server, so they are compiled but not run.
func connect() dbsql.Connection {
conn, err := dbsql.New(dbsql.Config{
Host: "localhost",
Port: "5432",
User: "postgres",
Password: "secret",
Database: "bank",
SSLMode: "disable",
MaxOpenConn: 10,
MaxIdleConn: 5,
})
if err != nil {
panic(err)
}
return conn
}
func main() {
conn := connect()
defer func() { _ = conn.Close() }()
ctx := context.Background()
var a account
if err := conn.Get(ctx, &a, "SELECT id, owner, balance FROM accounts WHERE id = $1", 1); err != nil {
panic(err)
}
fmt.Println(a.Owner, a.Balance)
var all []account
if err := conn.Select(ctx, &all, "SELECT id, owner, balance FROM accounts"); err != nil {
panic(err)
}
fmt.Println(len(all))
}
Output:
type Queryer ¶
type Queryer interface {
Get(ctx context.Context, dest any, query string, args ...any) error
Select(ctx context.Context, dest any, query string, args ...any) error
}
Queryer reads rows. Get scans a single row into dest and returns sql.ErrNoRows when there is none; Select scans every row into a slice.