db/db.go: TxFromContext/ContextWithTx for transaction propagation, Querier interface (QueryRow/Query/Exec), ScanRow generic helper, ClampLimit/ClampOffset pagination guards. db/postgres/postgres.go: NewPool with ping validation, Transactor backed by pgxpool (BeginTx → fn → commit/rollback), connOrTx helper that returns the active transaction from context or falls back to pool. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
71 lines
2.0 KiB
Go
71 lines
2.0 KiB
Go
// Package db provides shared helpers used by all database adapters.
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgconn"
|
|
)
|
|
|
|
// txKey is the context key used to store an active transaction.
|
|
type txKey struct{}
|
|
|
|
// TxFromContext returns the pgx.Tx stored in ctx by the Transactor, along
|
|
// with a boolean indicating whether a transaction is active.
|
|
func TxFromContext(ctx context.Context) (pgx.Tx, bool) {
|
|
tx, ok := ctx.Value(txKey{}).(pgx.Tx)
|
|
return tx, ok
|
|
}
|
|
|
|
// ContextWithTx returns a copy of ctx that carries tx.
|
|
// Called by the Transactor before invoking the user function.
|
|
func ContextWithTx(ctx context.Context, tx pgx.Tx) context.Context {
|
|
return context.WithValue(ctx, txKey{}, tx)
|
|
}
|
|
|
|
// Querier is the common query interface satisfied by both *pgxpool.Pool and
|
|
// pgx.Tx, allowing repo helpers to work with either.
|
|
type Querier interface {
|
|
QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
|
|
Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
|
|
Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
|
|
}
|
|
|
|
// ScanRow executes a single-row query against q and scans the result using
|
|
// scan. It wraps pgx.ErrNoRows so callers can detect missing rows without
|
|
// importing pgx directly.
|
|
func ScanRow[T any](ctx context.Context, q Querier, sql string, args []any, scan func(pgx.Row) (T, error)) (T, error) {
|
|
row := q.QueryRow(ctx, sql, args...)
|
|
val, err := scan(row)
|
|
if err != nil {
|
|
var zero T
|
|
if err == pgx.ErrNoRows {
|
|
return zero, fmt.Errorf("%w", pgx.ErrNoRows)
|
|
}
|
|
return zero, fmt.Errorf("ScanRow: %w", err)
|
|
}
|
|
return val, nil
|
|
}
|
|
|
|
// ClampLimit enforces the [1, max] range on limit, returning def when limit
|
|
// is zero or negative.
|
|
func ClampLimit(limit, def, max int) int {
|
|
if limit <= 0 {
|
|
return def
|
|
}
|
|
if limit > max {
|
|
return max
|
|
}
|
|
return limit
|
|
}
|
|
|
|
// ClampOffset returns 0 for negative offsets.
|
|
func ClampOffset(offset int) int {
|
|
if offset < 0 {
|
|
return 0
|
|
}
|
|
return offset
|
|
}
|