postgres

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 29, 2026 License: AGPL-3.0 Imports: 15 Imported by: 0

README

einherjar/db-postgres

version license go health

The runes carved in stone do not fade when the runemaster dies. They outlast the hand that carved them.

code.nochebuena.dev/einherjar/db-postgres is the PostgreSQL database component of the Einherjar framework. It wraps pgxpool behind a lifecycle-aware Component, exposes a uniform Executor interface for queries and transactions, and provides a UnitOfWork that injects the active transaction into context — so repositories never need to know whether they are inside a transaction or not.


Usage

Setup
import dbpg "code.nochebuena.dev/einherjar/db-postgres"

db := dbpg.New(logger, dbpg.DefaultConfig())
launcher.Append(db)   // OnInit opens pool; OnStop closes it
health.Register(db)   // PING health check; LevelCritical
Querying
// GetExecutor returns the pool when called outside a UnitOfWork.
rows, err := db.GetExecutor(ctx).Query(ctx, "SELECT id, name FROM users WHERE active = $1", true)
defer rows.Close()

var name string
err := db.GetExecutor(ctx).QueryRow(ctx, "SELECT name FROM users WHERE id = $1", id).Scan(&name)
Manual transaction
tx, err := db.Begin(ctx)
if err != nil {
    return err
}
defer tx.Rollback(ctx)

_, err = tx.Exec(ctx, "UPDATE accounts SET balance = balance - $1 WHERE id = $2", amount, fromID)
if err != nil {
    return err
}
return tx.Commit(ctx)

BeginTx is available when you need explicit isolation level control:

tx, err := db.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.Serializable})

UnitOfWork wraps the transaction in context so every call to GetExecutor inside uow.Do automatically returns the active transaction. Repositories require no changes.

uow := dbpg.NewUnitOfWork(logger, db)

err := uow.Do(ctx, func(ctx context.Context) error {
    _, err := db.GetExecutor(ctx).Exec(ctx, "INSERT INTO orders (...) VALUES (...)", ...)
    return err
})

If the function returns an error, the transaction is rolled back. If it returns nil, the transaction is committed.

Error handling
if err := db.HandleError(someErr); err != nil {
    // pgconn error codes mapped to xerrors:
    // 23505 unique_violation      → ErrAlreadyExists
    // 23503 foreign_key_violation → ErrPreconditionFailed
    // 23502 not_null_violation    → ErrInvalidInput
    // context.Canceled            → ErrCancelled
    // context.DeadlineExceeded    → ErrDeadlineExceeded
}

HandleError is also available as a package-level function: dbpg.HandleError(err).


Environment variables

Variable Required Default Description
EINHERJAR_PG_HOST Yes PostgreSQL host
EINHERJAR_PG_PORT No 5432 Listen port
EINHERJAR_PG_USER Yes Database user
EINHERJAR_PG_PASSWORD Yes Database password
EINHERJAR_PG_NAME Yes Database name
EINHERJAR_PG_SSL_MODE No disable disable, require, verify-full
EINHERJAR_PG_TIMEZONE No UTC Session timezone
EINHERJAR_PG_MAX_CONNS No 5 Maximum connections in pool
EINHERJAR_PG_MIN_CONNS No 2 Minimum idle connections
EINHERJAR_PG_MAX_CONN_LIFETIME No 1h Maximum connection lifetime
EINHERJAR_PG_MAX_CONN_IDLE_TIME No 30m Maximum idle time before connection is closed
EINHERJAR_PG_HEALTH_CHECK_PERIOD No 1m pgxpool background health check interval

Dependency graph

contracts  (zero dependencies)
    ↑
  core
    ↑
db-postgres  (contracts, core, pgx/v5, pgerrcode)
    ↑
  your app

Verification

cd db-postgres/
go build ./...
go vet ./...
go test ./...
gofmt -l .

The hall is built before the warriors arrive. That is the only guarantee worth making.

Documentation

Overview

Package postgres provides a pgx-backed PostgreSQL client with lifecycle management, health checks, and unit-of-work transaction support for Einherjar applications.

Overview

New returns a Component that manages a pgxpool.Pool connection pool, satisfies the lifecycle.Component lifecycle hooks (OnInit, OnStart, OnStop), and implements observability.Checkable with critical priority.

NewUnitOfWork wraps multiple repository operations in a single transaction via context injection — no transaction object is passed between functions.

Lifecycle Registration

db := postgres.New(logger, cfg)
launcher.Register(db)
health.Register(db)

Querying

Repository code receives a Provider or Executor and calls [Provider.GetExecutor] to obtain the active transaction (if inside a UnitOfWork) or the pool:

exec := db.GetExecutor(ctx)
row := exec.QueryRow(ctx, "SELECT id FROM users WHERE email = $1", email)
if err := row.Scan(&id); err != nil {
    return db.HandleError(err)  // maps pgx.ErrNoRows → ErrNotFound, etc.
}

Unit of Work

NewUnitOfWork wraps operations in a single transaction. The transaction is injected into the context; [Provider.GetExecutor] returns it automatically.

uow := postgres.NewUnitOfWork(logger, db)
err := uow.Do(ctx, func(ctx context.Context) error {
    exec := db.GetExecutor(ctx) // returns active Tx
    _, err := exec.Exec(ctx, "INSERT INTO orders ...")
    return err
})

Error Handling

HandleError translates pgx and PostgreSQL error codes into typed xerrors values. Call it at every point where a pgx error is first observed:

if err := row.Scan(&out); err != nil {
    return db.HandleError(err)
}

Mapped codes:

  • UniqueViolation → ErrAlreadyExists
  • ForeignKeyViolation → ErrInvalidInput
  • CheckViolation → ErrInvalidInput
  • pgx.ErrNoRows → ErrNotFound
  • all others → ErrInternal

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func HandleError

func HandleError(err error) error

HandleError maps pgx and PostgreSQL errors to typed xerrors values. Returns nil when err is nil. Also available as [Provider.HandleError].

Mapped codes:

  • UniqueViolation → ErrAlreadyExists
  • ForeignKeyViolation → ErrInvalidInput
  • CheckViolation → ErrInvalidInput
  • pgx.ErrNoRows → ErrNotFound
  • all others → ErrInternal

Types

type Component

type Component interface {
	lifecycle.Component
	observability.Checkable
	observability.Identifiable
	Provider
	// Stats returns connection pool statistics. Returns a zero-value stat
	// when the pool has not been initialized yet.
	Stats() *pgxpool.Stat
}

Component bundles the full postgres capability: lifecycle management, health reporting, and the database Provider interface. Register with launcher and health before starting.

func New

func New(logger logging.Logger, cfg Config) Component

New returns a Component backed by the given configuration. The pool is not created until OnInit is called.

type Config

type Config struct {
	Host              string `env:"EINHERJAR_PG_HOST,required"`
	Port              int    `env:"EINHERJAR_PG_PORT"                envDefault:"5432"`
	User              string `env:"EINHERJAR_PG_USER,required"`
	Password          string `env:"EINHERJAR_PG_PASSWORD,required"`
	Name              string `env:"EINHERJAR_PG_NAME,required"`
	SSLMode           string `env:"EINHERJAR_PG_SSL_MODE"            envDefault:"disable"`
	Timezone          string `env:"EINHERJAR_PG_TIMEZONE"            envDefault:"UTC"`
	MaxConns          int    `env:"EINHERJAR_PG_MAX_CONNS"           envDefault:"5"`
	MinConns          int    `env:"EINHERJAR_PG_MIN_CONNS"           envDefault:"2"`
	MaxConnLifetime   string `env:"EINHERJAR_PG_MAX_CONN_LIFETIME"   envDefault:"1h"`
	MaxConnIdleTime   string `env:"EINHERJAR_PG_MAX_CONN_IDLE_TIME"  envDefault:"30m"`
	HealthCheckPeriod string `env:"EINHERJAR_PG_HEALTH_CHECK_PERIOD" envDefault:"1m"`
}

Config holds PostgreSQL connection settings. Required fields must be supplied by the caller; optional fields have production-safe defaults via DefaultConfig.

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns a Config with all optional fields set to production-safe defaults. Callers must supply Host, Port, User, Password, and Name.

func (Config) DSN

func (c Config) DSN() string

DSN constructs a PostgreSQL connection string from the configuration.

type Executor

type Executor interface {
	// Exec executes a query that returns no rows.
	Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
	// Query executes a query that returns rows.
	Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
	// QueryRow executes a query that returns at most one row.
	QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
}

Executor is the shared query interface for both the connection pool and an active transaction. Repository code accepts Executor so it works identically inside and outside a UnitOfWork.

type Provider

type Provider interface {
	// GetExecutor returns the active transaction injected by [UnitOfWork] if one
	// is present in ctx, otherwise returns the connection pool.
	GetExecutor(ctx context.Context) Executor
	// Begin starts a new transaction with default options.
	Begin(ctx context.Context) (Tx, error)
	// BeginTx starts a new transaction with the given options.
	BeginTx(ctx context.Context, opts pgx.TxOptions) (Tx, error)
	// Ping verifies that the database connection is alive.
	Ping(ctx context.Context) error
	// HandleError maps a pgx or PostgreSQL error to a typed [xerrors] value.
	// Call this at every point where a pgx error is first observed.
	HandleError(err error) error
}

Provider is the database access interface consumed by repositories and services. All methods are safe for concurrent use.

type Tx

type Tx interface {
	Executor
	// Commit commits the transaction.
	Commit(ctx context.Context) error
	// Rollback rolls back the transaction. Safe to call after Commit.
	Rollback(ctx context.Context) error
}

Tx extends Executor with commit and rollback. Obtained via [Provider.Begin] or [Provider.BeginTx] when manual transaction control is needed. Prefer UnitOfWork for the common case of a single transactional unit.

type UnitOfWork

type UnitOfWork interface {
	// Do begins a transaction, calls fn with the enriched context, and commits
	// on success or rolls back on error. The original fn error is always returned
	// when fn fails, regardless of the rollback outcome.
	Do(ctx context.Context, fn func(ctx context.Context) error) error
}

UnitOfWork wraps a set of repository operations in a single database transaction. The transaction is injected into the context; [Provider.GetExecutor] returns it automatically so repository code requires no changes to participate.

func NewUnitOfWork

func NewUnitOfWork(logger logging.Logger, client Provider) UnitOfWork

NewUnitOfWork returns a UnitOfWork backed by the given client.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL