The package-doc examples showed a fictional launcher.Register / health.Register API (and launcher.Append as a package func). Corrected to the real wiring: lc.Append + web/health.NewHandler(...).ServeHTTP. Documentation-only. No code, API, or dependency changes; version stays v1.1.0 (the v1.1.0 tag is moved to include this fix). Verified by compiling the corrected pattern against the real API.
139 lines
4.2 KiB
Markdown
139 lines
4.2 KiB
Markdown
# einherjar/db-postgres
|
|
|
|
[](https://code.nochebuena.dev/einherjar/db-postgres)
|
|
[](LICENSE)
|
|
[](https://go.dev)
|
|
[]()
|
|
|
|
> 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
|
|
|
|
```go
|
|
import dbpg "code.nochebuena.dev/einherjar/db-postgres"
|
|
|
|
db := dbpg.New(logger, dbpg.DefaultConfig())
|
|
lc.Append(db) // OnInit opens pool; OnStop closes it
|
|
// db is observability.Checkable (PING health check, LevelCritical):
|
|
srv.Get("/health", health.NewHandler(logger, db).ServeHTTP)
|
|
```
|
|
|
|
### Querying
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
tx, err := db.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.Serializable})
|
|
```
|
|
|
|
### Unit of work (recommended)
|
|
|
|
`UnitOfWork` wraps the transaction in context so every call to `GetExecutor` inside `uow.Do` automatically returns the active transaction. Repositories require no changes.
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
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
|
|
|
|
```bash
|
|
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.*
|