2024-07-15 10:12:53 -04:00
|
|
|
package database
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"errors"
|
2024-11-17 21:46:10 -05:00
|
|
|
"fmt"
|
2024-07-15 10:12:53 -04:00
|
|
|
"strings"
|
|
|
|
|
2024-11-03 07:19:03 -05:00
|
|
|
"dynatron.me/x/stillbox/pkg/config"
|
2024-07-15 10:12:53 -04:00
|
|
|
sqlembed "dynatron.me/x/stillbox/sql"
|
|
|
|
"github.com/golang-migrate/migrate/v4"
|
|
|
|
_ "github.com/golang-migrate/migrate/v4/database/pgx/v5"
|
|
|
|
"github.com/golang-migrate/migrate/v4/source/iofs"
|
2024-11-15 13:42:38 -05:00
|
|
|
"github.com/jackc/pgx/v5"
|
2024-07-15 10:12:53 -04:00
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
2024-11-15 08:46:29 -05:00
|
|
|
"github.com/jackc/pgx/v5/tracelog"
|
|
|
|
"github.com/rs/zerolog/log"
|
2024-07-15 10:12:53 -04:00
|
|
|
)
|
|
|
|
|
2024-07-29 00:29:16 -04:00
|
|
|
// DB is a database handle.
|
2024-11-15 13:06:37 -05:00
|
|
|
|
2024-11-15 12:18:32 -05:00
|
|
|
//go:generate mockery
|
2024-11-17 21:46:10 -05:00
|
|
|
type Store interface {
|
2024-11-15 12:18:32 -05:00
|
|
|
Querier
|
|
|
|
talkgroupQuerier
|
|
|
|
|
|
|
|
DB() *Database
|
2024-11-17 21:46:10 -05:00
|
|
|
InTx(context.Context, func(Store) error, pgx.TxOptions) error
|
2024-11-15 12:18:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
type Database struct {
|
2024-07-23 21:35:02 -04:00
|
|
|
*pgxpool.Pool
|
|
|
|
*Queries
|
|
|
|
}
|
2024-07-15 10:12:53 -04:00
|
|
|
|
2024-11-15 12:18:32 -05:00
|
|
|
func (db *Database) DB() *Database {
|
|
|
|
return db
|
|
|
|
}
|
|
|
|
|
2024-11-17 21:46:10 -05:00
|
|
|
func (db *Database) InTx(ctx context.Context, f func(Store) error, opts pgx.TxOptions) error {
|
|
|
|
tx, err := db.DB().Pool.BeginTx(ctx, opts)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Tx begin: %w", err)
|
|
|
|
}
|
|
|
|
|
2024-11-20 11:07:58 -05:00
|
|
|
//nolint:errcheck
|
2024-11-17 21:46:10 -05:00
|
|
|
defer tx.Rollback(ctx)
|
|
|
|
|
|
|
|
dbtx := &Database{Pool: db.Pool, Queries: db.Queries.WithTx(tx)}
|
|
|
|
|
|
|
|
err = f(dbtx)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Tx: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = tx.Commit(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Tx commit: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-11-15 10:37:58 -05:00
|
|
|
type dbLogger struct{}
|
2024-11-15 08:46:29 -05:00
|
|
|
|
2024-11-15 10:37:58 -05:00
|
|
|
func (m dbLogger) Log(ctx context.Context, level tracelog.LogLevel, msg string, data map[string]any) {
|
2024-11-15 08:46:29 -05:00
|
|
|
log.Debug().Fields(data).Msg(msg)
|
|
|
|
}
|
|
|
|
|
2024-11-17 21:46:10 -05:00
|
|
|
func Close(c Store) {
|
|
|
|
c.(*Database).Pool.Close()
|
|
|
|
}
|
|
|
|
|
2024-07-29 00:29:16 -04:00
|
|
|
// NewClient creates a new DB using the provided config.
|
2024-11-17 21:46:10 -05:00
|
|
|
func NewClient(ctx context.Context, conf config.DB) (Store, error) {
|
2024-07-15 10:12:53 -04:00
|
|
|
dir, err := iofs.New(sqlembed.Migrations, "postgres/migrations")
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
m, err := migrate.NewWithSourceInstance("iofs", dir, strings.Replace(conf.Connect, "postgres://", "pgx5://", 1)) // yech
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = m.Up()
|
|
|
|
if err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
m.Close()
|
|
|
|
|
2024-10-23 08:55:19 -04:00
|
|
|
pgConf, err := pgxpool.ParseConfig(conf.Connect)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2024-11-15 10:37:58 -05:00
|
|
|
if conf.LogQueries {
|
|
|
|
pgConf.ConnConfig.Tracer = &tracelog.TraceLog{
|
|
|
|
Logger: dbLogger{},
|
|
|
|
LogLevel: tracelog.LogLevelTrace,
|
|
|
|
}
|
2024-11-15 08:46:29 -05:00
|
|
|
}
|
2024-11-15 10:37:58 -05:00
|
|
|
|
2024-10-23 08:55:19 -04:00
|
|
|
pool, err := pgxpool.NewWithConfig(ctx, pgConf)
|
2024-07-15 10:12:53 -04:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2024-11-15 12:18:32 -05:00
|
|
|
db := &Database{
|
2024-07-23 21:35:02 -04:00
|
|
|
Pool: pool,
|
|
|
|
Queries: New(pool),
|
|
|
|
}
|
|
|
|
|
2024-07-15 10:12:53 -04:00
|
|
|
return db, nil
|
|
|
|
}
|
|
|
|
|
2024-11-10 10:28:04 -05:00
|
|
|
type dBCtxKey string
|
2024-07-15 10:12:53 -04:00
|
|
|
|
2024-11-10 10:28:04 -05:00
|
|
|
const DBCtxKey dBCtxKey = "dbctx"
|
2024-07-15 10:12:53 -04:00
|
|
|
|
2024-07-29 00:29:16 -04:00
|
|
|
// FromCtx returns the database handle from the provided Context.
|
2024-11-17 21:46:10 -05:00
|
|
|
func FromCtx(ctx context.Context) Store {
|
|
|
|
c, ok := ctx.Value(DBCtxKey).(Store)
|
2024-07-15 10:12:53 -04:00
|
|
|
if !ok {
|
|
|
|
panic("no DB in context")
|
|
|
|
}
|
|
|
|
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
2024-07-29 00:29:16 -04:00
|
|
|
// CtxWithDB returns a Context with the provided database handle.
|
2024-11-17 21:46:10 -05:00
|
|
|
func CtxWithDB(ctx context.Context, conn Store) context.Context {
|
2024-11-10 10:28:04 -05:00
|
|
|
return context.WithValue(ctx, DBCtxKey, conn)
|
2024-07-15 10:12:53 -04:00
|
|
|
}
|
2024-07-23 21:35:02 -04:00
|
|
|
|
2024-07-29 00:29:16 -04:00
|
|
|
// IsNoRows is a convenience function that returns whether a returned error is a database
|
|
|
|
// no rows error.
|
2024-07-23 21:35:02 -04:00
|
|
|
func IsNoRows(err error) bool {
|
2024-11-15 13:42:38 -05:00
|
|
|
return errors.Is(err, pgx.ErrNoRows)
|
2024-07-23 21:35:02 -04:00
|
|
|
}
|