132 lines
3.9 KiB
Go
132 lines
3.9 KiB
Go
// Package database provides the database/sql lifecycle used by GoOmni projects.
|
|
// It deliberately does not import a database driver; each application chooses one.
|
|
package database
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Config struct {
|
|
Driver string `json:"driver"`
|
|
DSN string `json:"dsn"`
|
|
MaxOpenConns int `json:"maxOpenConns"`
|
|
MaxIdleConns int `json:"maxIdleConns"`
|
|
ConnMaxLifetime time.Duration `json:"connMaxLifetime"`
|
|
ConnMaxIdleTime time.Duration `json:"connMaxIdleTime"`
|
|
PingTimeout time.Duration `json:"pingTimeout"`
|
|
}
|
|
|
|
// MarshalJSON omits the connection credential from serialized diagnostics.
|
|
func (c Config) MarshalJSON() ([]byte, error) {
|
|
type plain Config
|
|
safe := plain(c)
|
|
safe.DSN = ""
|
|
return json.Marshal(safe)
|
|
}
|
|
|
|
func DefaultConfig() Config { return DefaultPoolConfig().config() }
|
|
|
|
// PoolConfig returns driver-independent connection pool settings.
|
|
func (c Config) PoolConfig() PoolConfig {
|
|
return PoolConfig{
|
|
MaxOpenConns: c.MaxOpenConns, MaxIdleConns: c.MaxIdleConns,
|
|
ConnMaxLifetime: c.ConnMaxLifetime, ConnMaxIdleTime: c.ConnMaxIdleTime,
|
|
PingTimeout: c.PingTimeout,
|
|
}
|
|
}
|
|
|
|
func (c Config) Validate() error {
|
|
if strings.TrimSpace(c.Driver) == "" {
|
|
return errors.New("database driver is required")
|
|
}
|
|
if strings.TrimSpace(c.DSN) == "" {
|
|
return errors.New("database DSN is required")
|
|
}
|
|
return c.PoolConfig().Validate()
|
|
}
|
|
|
|
// DB owns a connection pool and exposes the embedded database/sql methods.
|
|
type DB struct {
|
|
*sql.DB
|
|
config Config
|
|
}
|
|
|
|
func Open(ctx context.Context, cfg Config) (*DB, error) {
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
if err := cfg.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
db, err := sql.Open(cfg.Driver, cfg.DSN)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open database: %w", err)
|
|
}
|
|
return initializePool(ctx, db, cfg)
|
|
}
|
|
|
|
// initializePool is the shared ownership boundary for both creation paths.
|
|
// All configuration is validated before creating the sql.DB.
|
|
func initializePool(ctx context.Context, db *sql.DB, cfg Config) (*DB, error) {
|
|
db.SetMaxOpenConns(cfg.MaxOpenConns)
|
|
db.SetMaxIdleConns(cfg.MaxIdleConns) // Zero explicitly disables idle connections.
|
|
db.SetConnMaxLifetime(cfg.ConnMaxLifetime)
|
|
db.SetConnMaxIdleTime(cfg.ConnMaxIdleTime)
|
|
checkCtx, cancel := context.WithTimeout(ctx, cfg.PingTimeout)
|
|
defer cancel()
|
|
if err := db.PingContext(checkCtx); err != nil {
|
|
pingErr := fmt.Errorf("ping database: %w", err)
|
|
if closeErr := db.Close(); closeErr != nil {
|
|
return nil, errors.Join(pingErr, fmt.Errorf("close database after failed ping: %w", closeErr))
|
|
}
|
|
return nil, pingErr
|
|
}
|
|
return &DB{DB: db, config: cfg}, nil
|
|
}
|
|
|
|
// Config returns the original DSN configuration. Pools opened with a Connector
|
|
// have empty Driver/DSN fields; their connector cannot be reconstructed from Config.
|
|
func (db *DB) Config() Config { return db.config }
|
|
|
|
func (db *DB) Health(ctx context.Context) error {
|
|
if db == nil || db.DB == nil {
|
|
return errors.New("database is not initialized")
|
|
}
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
checkCtx, cancel := context.WithTimeout(ctx, db.config.PingTimeout)
|
|
defer cancel()
|
|
return db.PingContext(checkCtx)
|
|
}
|
|
|
|
// InTx commits only when work returns nil. Any work error or panic rolls back.
|
|
func (db *DB) InTx(ctx context.Context, options *sql.TxOptions, work func(*sql.Tx) error) (err error) {
|
|
if db == nil || db.DB == nil {
|
|
return errors.New("database is not initialized")
|
|
}
|
|
tx, err := db.BeginTx(ctx, options)
|
|
if err != nil {
|
|
return fmt.Errorf("begin transaction: %w", err)
|
|
}
|
|
defer func() {
|
|
rollbackErr := tx.Rollback()
|
|
if rollbackErr != nil && !errors.Is(rollbackErr, sql.ErrTxDone) {
|
|
err = errors.Join(err, fmt.Errorf("rollback transaction: %w", rollbackErr))
|
|
}
|
|
}()
|
|
if err = work(tx); err != nil {
|
|
return err
|
|
}
|
|
if err = tx.Commit(); err != nil {
|
|
return fmt.Errorf("commit transaction: %w", err)
|
|
}
|
|
return nil
|
|
}
|