package database import ( "context" "database/sql" "fmt" "log/slog" "time" "gorm.io/driver/postgres" "gorm.io/gorm" ) type OpenOptions struct { MaxOpenConns int MaxIdleConns int ConnMaxIdleTime time.Duration ConnMaxLifetime time.Duration } func defaultOpenOptions() OpenOptions { return OpenOptions{MaxOpenConns: 20, MaxIdleConns: 5, ConnMaxIdleTime: 5 * time.Minute, ConnMaxLifetime: 30 * time.Minute} } func normalizeOpenOptions(options []OpenOptions) (OpenOptions, error) { if len(options) == 0 { return defaultOpenOptions(), nil } value := options[0] if value.MaxOpenConns < 1 || value.MaxIdleConns < 0 || value.MaxIdleConns > value.MaxOpenConns { return OpenOptions{}, fmt.Errorf("invalid database connection pool limits") } if value.ConnMaxIdleTime <= 0 || value.ConnMaxLifetime <= 0 { return OpenOptions{}, fmt.Errorf("database connection lifetimes must be positive") } return value, nil } func Open(ctx context.Context, dsn string, appLogger *slog.Logger, options ...OpenOptions) (*gorm.DB, error) { poolOptions, err := normalizeOpenOptions(options) if err != nil { return nil, err } db, err := gorm.Open(postgres.New(postgres.Config{DSN: dsn}), &gorm.Config{ Logger: newStructuredGORMLogger(appLogger), TranslateError: true, }) if err != nil { return nil, fmt.Errorf("open postgres: %w", err) } sqlDB, err := db.DB() if err != nil { return nil, fmt.Errorf("get sql db: %w", err) } sqlDB.SetMaxOpenConns(poolOptions.MaxOpenConns) sqlDB.SetMaxIdleConns(poolOptions.MaxIdleConns) sqlDB.SetConnMaxIdleTime(poolOptions.ConnMaxIdleTime) sqlDB.SetConnMaxLifetime(poolOptions.ConnMaxLifetime) pingCtx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() if err := sqlDB.PingContext(pingCtx); err != nil { _ = sqlDB.Close() return nil, fmt.Errorf("ping postgres: %w", err) } appLogger.Info("database connected") return db, nil } func Close(db *gorm.DB) error { sqlDB, err := db.DB() if err != nil { return err } return sqlDB.Close() } func SQLDB(db *gorm.DB) (*sql.DB, error) { return db.DB() }