package database import ( "api_bb/migrations" "database/sql" "fmt" "strings" "time" "github.com/golang-migrate/migrate/v4" migratepg "github.com/golang-migrate/migrate/v4/database/postgres" "github.com/golang-migrate/migrate/v4/source/iofs" "go.uber.org/zap" gormpg "gorm.io/driver/postgres" "gorm.io/gorm" "api_bb/pkg/logger" ) type Database struct { DB *gorm.DB cfg *Config } type Config struct { URL string Schema string } func NewDatabase(cfg *Config) *Database { if cfg.Schema == "" { cfg.Schema = "public" } return &Database{ cfg: cfg, } } func (d *Database) Connect() error { zapLogger := logger.Get() zapLogger.Info("attempting to connect to database", zap.String("host", ExtractHostFromDSN(d.cfg.URL)), zap.String("database", ExtractDBNameFromDSN(d.cfg.URL)), zap.String("schema", d.cfg.Schema), ) dsn := d.cfg.URL if d.cfg.Schema != "public" { dsn = dsn + fmt.Sprintf(" search_path=%s", d.cfg.Schema) } db, err := gorm.Open(gormpg.Open(dsn), &gorm.Config{}) if err != nil { zapLogger.Error("failed to connect to database", zap.Error(err), zap.String("database_url", MaskPassword(d.cfg.URL)), ) return fmt.Errorf("failed to connect to database: %w", err) } d.DB = db zapLogger.Info("Configure connection pool") sqlDB, err := d.DB.DB() if err != nil { return fmt.Errorf("failed to get underlying sql.DB: %w", err) } sqlDB.SetMaxOpenConns(25) sqlDB.SetMaxIdleConns(10) sqlDB.SetConnMaxLifetime(30 * time.Minute) zapLogger.Info("Run database migrations") if err := d.runMigrations(sqlDB); err != nil { return fmt.Errorf("failed to run migrations: %w", err) } zapLogger.Info("Migrations completed successfully") zapLogger.Info("successfully connected to database", zap.String("host", ExtractHostFromDSN(d.cfg.URL)), zap.String("database", ExtractDBNameFromDSN(d.cfg.URL)), ) return nil } func (d *Database) runMigrations(sqlDB *sql.DB) error { zapLogger := logger.Get() source, err := iofs.New(migrations.FS, ".") if err != nil { return fmt.Errorf("failed to create migration source: %w", err) } driver, err := migratepg.WithInstance(sqlDB, &migratepg.Config{}) if err != nil { return fmt.Errorf("failed to create postgres driver: %w", err) } m, err := migrate.NewWithInstance("iofs", source, "postgres", driver) if err != nil { return fmt.Errorf("failed to create migrate instance: %w", err) } if err := m.Up(); err != nil && err != migrate.ErrNoChange { zapLogger.Error("Migration error", zap.Error(err)) return fmt.Errorf("failed to apply migrations: %w", err) } return nil } func (d *Database) Ping() error { zapLogger := logger.Get() sqlDB, err := d.DB.DB() if err != nil { zapLogger.Error("failed to get database instance", zap.Error(err)) return fmt.Errorf("failed to get database instance: %w", err) } if err := sqlDB.Ping(); err != nil { zapLogger.Error("database ping failed", zap.Error(err)) return fmt.Errorf("database ping failed: %w", err) } zapLogger.Info("database ping successful") return nil } func (d *Database) Close() error { zapLogger := logger.Get() if d.DB == nil { return nil } sqlDB, err := d.DB.DB() if err != nil { zapLogger.Error("failed to get database instance for closing", zap.Error(err)) return fmt.Errorf("failed to get database instance: %w", err) } zapLogger.Info("closing database connection") if err := sqlDB.Close(); err != nil { zapLogger.Error("failed to close database connection", zap.Error(err)) return fmt.Errorf("failed to close database connection: %w", err) } zapLogger.Info("database connection closed successfully") return nil } func ExtractHostFromDSN(dsn string) string { parts := strings.Split(dsn, " ") for _, part := range parts { if strings.HasPrefix(part, "host=") { return strings.TrimPrefix(part, "host=") } } return "unknown" } func ExtractDBNameFromDSN(dsn string) string { parts := strings.Split(dsn, " ") for _, part := range parts { if strings.HasPrefix(part, "dbname=") { return strings.TrimPrefix(part, "dbname=") } } return "unknown" } func MaskPassword(dsn string) string { parts := strings.Split(dsn, " ") for i, part := range parts { if strings.HasPrefix(part, "password=") { parts[i] = "password=***" break } } return strings.Join(parts, " ") }