package db import ( "context" "database/sql" "embed" "fmt" "strconv" "time" "music-server/internal/logging" "github.com/golang-migrate/migrate/v4" "github.com/golang-migrate/migrate/v4/database/postgres" _ "github.com/golang-migrate/migrate/v4/database/postgres" _ "github.com/golang-migrate/migrate/v4/source/file" "github.com/golang-migrate/migrate/v4/source/iofs" "github.com/jackc/pgx/v5/pgxpool" _ "github.com/lib/pq" "go.uber.org/zap" ) // TODO: Remove these global variables once test_helpers.go is fully migrated to use Database struct var Dbpool *pgxpool.Pool var Ctx = context.Background() //go:embed "migrations/*.sql" var MigrationsFs embed.FS func InitDB(host string, port string, user string, password string, dbname string) { psqlInfo := fmt.Sprintf("host=%s port=%s user=%s "+ "password=%s dbname=%s sslmode=disable", host, port, user, password, dbname) logging.GetLogger().Debug("Database connection info", zap.String("host", host), zap.String("port", port), zap.String("dbname", dbname)) var err error Dbpool, err = pgxpool.New(Ctx, psqlInfo) if err != nil { logging.GetLogger().Fatal("Unable to connect to database", zap.String("error", err.Error())) } var success string err = Dbpool.QueryRow(Ctx, "select 'Successfully connected!'").Scan(&success) if err != nil { logging.GetLogger().Fatal("Database query failed", zap.String("error", err.Error())) } logging.GetLogger().Info("Database connected", zap.String("status", success)) } func CloseDb() { logging.GetLogger().Info("Closing database connection") Dbpool.Close() } func Testf() { rows, dbErr := Dbpool.Query(Ctx, "select game_name from game") if dbErr != nil { logging.GetLogger().Fatal("Query failed", zap.String("error", dbErr.Error())) } for rows.Next() { var gameName string dbErr = rows.Scan(&gameName) if dbErr != nil { logging.GetLogger().Error("Row scan failed", zap.String("error", dbErr.Error())) } logging.GetLogger().Debug("Game found", zap.String("name", gameName)) } } func ResetGameIdSeq() { _, err := Dbpool.Query(Ctx, "SELECT setval('game_id_seq', (SELECT MAX(id) FROM game)+1);") if err != nil { logging.GetLogger().Error("Failed to reset game ID sequence", zap.String("error", err.Error())) } } func createDb(host string, port string, user string, password string, dbname string) { // Connect to the default postgres database to create new database // In PostgreSQL, we need to connect to an existing database (postgres) to create a new one conninfo := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=postgres sslmode=disable", host, port, user, password) db, err := sql.Open("postgres", conninfo) defer db.Close() if err != nil { logging.GetLogger().Fatal("Failed to connect for database creation", zap.String("error", err.Error())) } _, err = db.Exec("create database " + dbname) if err != nil { //handle the error logging.GetLogger().Fatal("Failed to create database", zap.String("error", err.Error())) } logging.GetLogger().Info("Database created", zap.String("dbname", dbname)) } func Migrate_db(host string, port string, user string, password string, dbname string) { migrationInfo := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", user, password, host, port, dbname) logging.GetLogger().Debug("Migration info", zap.String("url", migrationInfo)) db, err := sql.Open("postgres", migrationInfo) if err != nil { logging.GetLogger().Error("Failed to open database for migration", zap.String("error", err.Error())) } driver, err := postgres.WithInstance(db, &postgres.Config{}) if err != nil { logging.GetLogger().Error("Failed to create migration driver", zap.String("error", err.Error())) } files, err := iofs.New(MigrationsFs, "migrations") if err != nil { logging.GetLogger().Fatal("Failed to create migration files", zap.String("error", err.Error())) } m, err := migrate.NewWithInstance("iofs", files, "postgres", driver) if err != nil { logging.GetLogger().Fatal("Failed to create migrator", zap.String("error", err.Error())) } /*m, err := migrate.NewWithDatabaseInstance( "file://./db/migrations/", "postgres", driver) if err != nil { logging.GetLogger().Error("Migration setup error", zap.String("error", err.Error())) }*/ version, _, err := m.Version() if err != nil { logging.GetLogger().Error("Failed to get migration version", zap.String("error", err.Error())) } logging.GetLogger().Info("Migration version before", zap.Uint("version", version)) //err = m.Force(1) //err = m.Up() // or m.Steps(2) if you want to explicitly set the number of migrations to run //if err != nil { // logging.GetLogger().Error("Force migration error", zap.String("error", err.Error())) //} // Use Up() to apply all pending migrations instead of Migrate(2) err = m.Up() if err != nil { if err == migrate.ErrNoChange { logging.GetLogger().Info("Database already up to date") } else { logging.GetLogger().Error("Migration error", zap.String("error", err.Error())) } } else { versionAfter, _, err := m.Version() if err != nil { logging.GetLogger().Error("Failed to get migration version after", zap.String("error", err.Error())) } else { logging.GetLogger().Info("Migrated to version", zap.Uint("version", versionAfter)) } } logging.GetLogger().Info("Migration completed") db.Close() } // Health checks the health of the database connection by pinging the database. // It returns a map with keys indicating various health statistics. func Health() map[string]string { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() stats := make(map[string]string) // Ping the database //err := s.db.PingContext(ctx) err := Dbpool.Ping(ctx) if err != nil { stats["status"] = "down" stats["error"] = fmt.Sprintf("db down: %v", err) logging.GetLogger().Fatal("Database health check failed", zap.String("error", err.Error())) return stats } // Database is up, add more statistics stats["status"] = "up" stats["message"] = "It's healthy" // Get database stats (like open connections, in use, idle, etc.) //dbStats := s.db.Stats() dbStats := Dbpool.Stat() //stats["open_connections"] = strconv.Itoa(dbStats.OpenConnections) stats["open_connections"] = strconv.Itoa(int(dbStats.NewConnsCount())) //stats["in_use"] = strconv.Itoa(dbStats.InUse) stats["in_use"] = strconv.Itoa(int(dbStats.AcquiredConns())) //stats["idle"] = strconv.Itoa(dbStats.Idle) stats["idle"] = strconv.Itoa(int(dbStats.IdleConns())) //stats["wait_count"] = strconv.FormatInt(dbStats.WaitCount, 10) stats["wait_count"] = strconv.FormatInt(dbStats.AcquireCount(), 10) //stats["wait_duration"] = dbStats.WaitDuration.String() stats["wait_duration"] = dbStats.AcquireDuration().String() //stats["max_idle_closed"] = strconv.FormatInt(dbStats.MaxIdleClosed, 10) stats["max_idle_closed"] = strconv.FormatInt(dbStats.MaxIdleDestroyCount(), 10) //stats["max_lifetime_closed"] = strconv.FormatInt(dbStats.MaxLifetimeClosed, 10) stats["max_lifetime_closed"] = strconv.FormatInt(dbStats.MaxLifetimeDestroyCount(), 10) // Evaluate stats to provide a health message if int(dbStats.NewConnsCount()) > 40 { // Assuming 50 is the max for this example stats["message"] = "The database is experiencing heavy load." } if dbStats.AcquireCount() > 1000 { stats["message"] = "The database has a high number of wait events, indicating potential bottlenecks." } if dbStats.MaxIdleDestroyCount() > int64(dbStats.NewConnsCount())/2 { stats["message"] = "Many idle connections are being closed, consider revising the connection pool settings." } if dbStats.MaxLifetimeDestroyCount() > int64(dbStats.NewConnsCount())/2 { stats["message"] = "Many connections are being closed due to max lifetime, consider increasing max lifetime or revising the connection usage pattern." } return stats }