- Fix 'string(d)' int-to-rune conversion in rpg_test.go by using fmt.Sprintf - Fix ComputeDailyTotals dayStart logic: initial state should be StateWorking when dayStart > 0, correctly counting work from dayStart to first out event - Update README project structure to reflect new domain/handler/repo layout - Implement missing achievements: rpg_pioneer (on RPG enable) and salary_setter (on first rate set) - Add tryUnlockAchievement helper to Handler
222 lines
6.1 KiB
Go
222 lines
6.1 KiB
Go
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"))
|
|
}
|
|
}
|
|
}
|