package repo import ( "database/sql" "embed" "log/slog" "time" "github.com/pressly/goose/v3" ) //go:embed migrations/*.sql var migrationFS embed.FS // runMigrations handles all database migrations using goose. func runMigrations(db *sql.DB) error { needsOldMigrate := hasTable(db, "events_old") || (hasTable(db, "events") && !hasCol(db, "events", "user_id")) if needsOldMigrate { slog.Info("migrating from old schema to new schema") for _, t := range []string{"events", "days_off", "settings"} { if hasTable(db, t) { if _, err := db.Exec("ALTER TABLE " + t + " RENAME TO " + t + "_old"); err != nil { return err } } } } goose.SetBaseFS(migrationFS) if err := goose.SetDialect("sqlite3"); err != nil { return err } if err := goose.Up(db, "migrations"); err != nil { return err } if needsOldMigrate { if err := migrateFromOldSchemaData(db); err != nil { return err } } return nil } func hasTable(db *sql.DB, name string) bool { var exists int if err := db.QueryRow("SELECT COUNT(1) FROM sqlite_master WHERE type='table' AND name=?", name).Scan(&exists); err != nil { return false } return exists > 0 } func hasCol(db *sql.DB, table, column string) bool { rows, err := db.Query("PRAGMA table_info(" + table + ")") if err != nil { return false } defer rows.Close() for rows.Next() { var cid int var name, ctype string var notnull, pk int var dflt sql.NullString if err := rows.Scan(&cid, &name, &ctype, ¬null, &dflt, &pk); err != nil { return false } if name == column { return true } } return false } func migrateFromOldSchemaData(db *sql.DB) error { migrateUsers(db) migrateDaysOff(db) migrateEvents(db) migrateSettings(db) for _, t := range []string{"days_off_old", "settings_old", "events_old"} { if hasTable(db, t) { if _, err := db.Exec("DROP TABLE IF EXISTS " + t); err != nil { slog.Warn("failed to drop old table", "table", t, "error", err) } } } slog.Info("migration complete") return nil } func migrateUsers(db *sql.DB) { if !hasTable(db, "events_old") { return } rows, err := db.Query("SELECT DISTINCT chat_id FROM events_old") if err != nil { slog.Warn("migration: failed to query events_old for users", "error", err) return } defer rows.Close() for rows.Next() { var chatID int64 if err := rows.Scan(&chatID); err != nil { slog.Warn("migration: scan failed", "error", err) continue } if _, err := db.Exec( "INSERT OR IGNORE INTO users (chat_id, timezone) VALUES (?, 'Asia/Tehran')", chatID, ); err != nil { slog.Warn("migration: failed to insert user", "chat_id", chatID, "error", err) } } } func migrateDaysOff(db *sql.DB) { if !hasTable(db, "days_off_old") { return } rows, err := db.Query("SELECT chat_id, date, reason FROM days_off_old") if err != nil { slog.Warn("migration: failed to query days_off_old", "error", err) return } defer rows.Close() for rows.Next() { var chatID int64 var date, reason string if err := rows.Scan(&chatID, &date, &reason); err != nil { slog.Warn("migration: scan failed", "error", err) continue } var userID int64 if err := db.QueryRow("SELECT id FROM users WHERE chat_id=?", chatID).Scan(&userID); err != nil { slog.Warn("migration: user not found", "chat_id", chatID, "error", err) continue } if _, err := db.Exec( "INSERT INTO days (user_id, date, is_day_off, day_off_reason) VALUES (?, ?, 1, ?) ON CONFLICT(user_id, date) DO UPDATE SET is_day_off=1, day_off_reason=excluded.day_off_reason", userID, date, reason, ); err != nil { slog.Warn("migration: failed to insert day_off", "user_id", userID, "date", date, "error", err) } } } func migrateEvents(db *sql.DB) { if !hasTable(db, "events_old") { return } rows, err := db.Query("SELECT chat_id, event_type, occurred_at, note, created_at FROM events_old ORDER BY occurred_at ASC") if err != nil { slog.Warn("migration: failed to query events_old", "error", err) return } defer rows.Close() for rows.Next() { var chatID int64 var eventType, note string var occurredAt, createdAt int64 if err := rows.Scan(&chatID, &eventType, &occurredAt, ¬e, &createdAt); err != nil { slog.Warn("migration: scan failed", "error", err) continue } var userID int64 if err := db.QueryRow("SELECT id FROM users WHERE chat_id=?", chatID).Scan(&userID); err != nil { slog.Warn("migration: user not found", "chat_id", chatID, "error", err) continue } date := time.Unix(occurredAt, 0).UTC().Format("2006-01-02") if _, err := db.Exec("INSERT OR IGNORE INTO days (user_id, date) VALUES (?, ?)", userID, date); err != nil { slog.Warn("migration: failed to insert day", "user_id", userID, "date", date, "error", err) continue } var dayID int64 if err := db.QueryRow("SELECT id FROM days WHERE user_id=? AND date=?", userID, date).Scan(&dayID); err != nil { slog.Warn("migration: day not found", "user_id", userID, "date", date, "error", err) continue } if _, err := db.Exec( "INSERT INTO events (user_id, day_id, event_type, occurred_at, note, created_at) VALUES (?, ?, ?, ?, ?, ?)", userID, dayID, eventType, occurredAt, note, createdAt, ); err != nil { slog.Warn("migration: failed to insert event", "user_id", userID, "error", err) } } } func migrateSettings(db *sql.DB) { if !hasTable(db, "settings_old") { return } rows, err := db.Query("SELECT chat_id, key, value FROM settings_old") if err != nil { slog.Warn("migration: failed to query settings_old", "error", err) return } defer rows.Close() for rows.Next() { var chatID int64 var key, value string if err := rows.Scan(&chatID, &key, &value); err != nil { slog.Warn("migration: scan failed", "error", err) continue } if key == "remote_flag" { var userID int64 if err := db.QueryRow("SELECT id FROM users WHERE chat_id=?", chatID).Scan(&userID); err != nil { slog.Warn("migration: user not found", "chat_id", chatID, "error", err) continue } wt := int64(1) if value == "remote" { wt = 2 } db.Exec("UPDATE days SET current_work_type_id=? WHERE user_id=? AND date=?", wt, userID, time.Now().UTC().Format("2006-01-02")) } } }