Files
db-postgres/config.go
Rene Nochebuena 4b58f8e3d9 feat(db-postgres): initial implementation — pgx pool with lifecycle and UnitOfWork (v1.0.0)
Introduces code.nochebuena.dev/einherjar/db-postgres — the PostgreSQL database
starter for the Einherjar framework. Absorbs the postgres package from micro-lib,
replacing fmt.Errorf wrapping with core/xerrors and migrating from pgx v4 to pgx v5.

Interfaces (CT-6: one TypeSpec per file):
- Executor — Exec, Query, QueryRow (pgx-native types)
- Tx — Executor + Commit(ctx), Rollback(ctx)
- Provider — GetExecutor, Begin, BeginTx, Ping, HandleError
- Component — lifecycle.Component + observability.Checkable + Provider + Stats()
- UnitOfWork — Do(ctx, fn)

Implementation:
- New(logger, cfg) Component — pool not created until OnInit
- OnInit: pgxpool.NewWithConfig with duration parsing; 30s timeout
- OnStart: PING with 5s timeout; logs "postgres: connected"
- OnStop: pool.Close(); logs "postgres: closing pool"
- GetExecutor: returns active Tx from context (ctxTxKey) or pool; nil-safe
- Begin/BeginTx: wraps pgx.TxOptions; wrapped in xerrors on error
- HealthCheck: delegates to Ping; Priority LevelCritical
- Stats() *pgxpool.Stat — zero value when pool uninitialized
- NewUnitOfWork(logger, provider) UnitOfWork — Begin+inject+commit/rollback
- HandleError: UniqueViolation→ErrAlreadyExists, ForeignKey/Check→ErrInvalidInput,
  pgx.ErrNoRows→ErrNotFound, all others→ErrInternal

Config (EINHERJAR_PG_* env vars):
  Host, Port(5432), User, Password, Name, SSLMode(disable), Timezone(UTC),
  MaxConns(5), MinConns(2), MaxConnLifetime(1h), MaxConnIdleTime(30m),
  HealthCheckPeriod(1m)

- Component interface embeds observability.Identifiable; identifiable.go implements
  ModulePath and ModuleVersion via runtime/debug.ReadBuildInfo() — prints in launcher banner
2026-05-29 15:50:20 +00:00

54 lines
1.9 KiB
Go

package postgres
import (
"fmt"
"net/url"
)
// Config holds PostgreSQL connection settings. Required fields must be supplied
// by the caller; optional fields have production-safe defaults via [DefaultConfig].
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"`
}
// DefaultConfig returns a Config with all optional fields set to production-safe
// defaults. Callers must supply Host, Port, User, Password, and Name.
func DefaultConfig() Config {
return Config{
Port: 5432,
SSLMode: "disable",
Timezone: "UTC",
MaxConns: 5,
MinConns: 2,
MaxConnLifetime: "1h",
MaxConnIdleTime: "30m",
HealthCheckPeriod: "1m",
}
}
// DSN constructs a PostgreSQL connection string from the configuration.
func (c Config) DSN() string {
u := &url.URL{
Scheme: "postgres",
User: url.UserPassword(c.User, c.Password),
Host: fmt.Sprintf("%s:%d", c.Host, c.Port),
Path: "/" + c.Name,
}
q := u.Query()
q.Set("sslmode", c.SSLMode)
q.Set("timezone", c.Timezone)
u.RawQuery = q.Encode()
return u.String()
}