126 lines
2.6 KiB
Go
126 lines
2.6 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgconn"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"botpontos/internal/domain"
|
|
)
|
|
|
|
// Store reúne as consultas da aplicação. As queries são escritas à mão com pgx.
|
|
//
|
|
// Convenções:
|
|
// - parâmetros de enum usam $n::text::<tipo>, para o pgx enviar texto;
|
|
// - datas (AAAA-MM-DD) trafegam como string, com $n::text::date e to_char na leitura;
|
|
// - filtros opcionais usam ($n::<tipo> IS NULL OR ...) com ponteiros nil.
|
|
type Store struct {
|
|
pool *pgxpool.Pool
|
|
}
|
|
|
|
func NewStore(pool *pgxpool.Pool) *Store {
|
|
return &Store{pool: pool}
|
|
}
|
|
|
|
// Ping é usado pelos healthchecks.
|
|
func (s *Store) Ping(ctx context.Context) error {
|
|
return s.pool.Ping(ctx)
|
|
}
|
|
|
|
type querier interface {
|
|
Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
|
|
Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
|
|
QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
|
|
}
|
|
|
|
type scanner interface {
|
|
Scan(dest ...any) error
|
|
}
|
|
|
|
// inTx executa fn numa transação; qualquer erro desfaz tudo.
|
|
func (s *Store) inTx(ctx context.Context, fn func(q querier) error) error {
|
|
tx, err := s.pool.Begin(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = tx.Rollback(ctx) }()
|
|
if err := fn(tx); err != nil {
|
|
return err
|
|
}
|
|
return tx.Commit(ctx)
|
|
}
|
|
|
|
// mapErr traduz erros do pgx para os erros do domínio.
|
|
func mapErr(err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.ErrNotFound
|
|
}
|
|
var pgErr *pgconn.PgError
|
|
if errors.As(err, &pgErr) && pgErr.Code == "23505" {
|
|
return domain.ErrConflict
|
|
}
|
|
return err
|
|
}
|
|
|
|
func expectOne(tag pgconn.CommandTag, err error) error {
|
|
if err != nil {
|
|
return mapErr(err)
|
|
}
|
|
if tag.RowsAffected() == 0 {
|
|
return domain.ErrNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// collect lê todas as linhas com a função de leitura informada.
|
|
func collect[T any](rows pgx.Rows, err error, read func(scanner) (T, error)) ([]T, error) {
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []T
|
|
for rows.Next() {
|
|
item, err := read(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, item)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func cols(prefix string, names ...string) string {
|
|
out := ""
|
|
for i, n := range names {
|
|
if i > 0 {
|
|
out += ", "
|
|
}
|
|
out += prefix + n
|
|
}
|
|
return out
|
|
}
|
|
|
|
func strPtr(s string) *string {
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
return &s
|
|
}
|
|
|
|
func pauseReasonPtr(r *domain.PauseReason) *string {
|
|
if r == nil {
|
|
return nil
|
|
}
|
|
v := string(*r)
|
|
return &v
|
|
}
|