package database import ( "fmt" "io/ioutil" "log" "os" "path/filepath" "sort" "strings" "github.com/simpletodoapp/go-todo-app/internal/config" ) // MigrationRunner handles database migrations type MigrationRunner struct { db *Database config *config.Config } // NewMigrationRunner creates a new migration runner func NewMigrationRunner(db *Database, cfg *config.Config) *MigrationRunner { return &MigrationRunner{ db: db, config: cfg, } } // RunMigrations executes all pending migrations func (m *MigrationRunner) RunMigrations(migrationsPath string) error { log.Println("Running migrations...") // Get all migration files files, err := getMigrationFiles(migrationsPath) if err != nil { return err } if len(files) == 0 { log.Println("No migration files found") return nil } // Create migrations table if it doesn't exist err = m.createMigrationsTable() if err != nil { return err } // Get applied migrations appliedMigrations, err := m.getAppliedMigrations() if err != nil { return err } // Apply pending migrations for _, file := range files { migrationName := filepath.Base(file) if !contains(appliedMigrations, migrationName) { log.Printf("Applying migration: %s", migrationName) // Read migration file content, err := ioutil.ReadFile(file) if err != nil { return fmt.Errorf("failed to read migration file: %v", err) } // Split into up and down migrations parts := strings.Split(string(content), "-- Down migration") upMigration := parts[0] // Execute migration tx := m.db.DB.Begin() if err := tx.Exec(upMigration).Error; err != nil { tx.Rollback() return fmt.Errorf("failed to apply migration: %v", err) } // Record applied migration if err := tx.Exec("INSERT INTO migrations (name, applied_at) VALUES (?, CURRENT_TIMESTAMP)", migrationName).Error; err != nil { tx.Rollback() return fmt.Errorf("failed to record migration: %v", err) } tx.Commit() log.Printf("Migration applied: %s", migrationName) } else { log.Printf("Migration already applied: %s", migrationName) } } log.Println("Migrations completed successfully") return nil } // createMigrationsTable creates the migrations table if it doesn't exist func (m *MigrationRunner) createMigrationsTable() error { query := ` CREATE TABLE IF NOT EXISTS migrations ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, applied_at DATETIME NOT NULL ) ` return m.db.DB.Exec(query).Error } // getAppliedMigrations gets the list of already applied migrations func (m *MigrationRunner) getAppliedMigrations() ([]string, error) { var migrations []string var records []struct { Name string } result := m.db.DB.Raw("SELECT name FROM migrations ORDER BY id ASC").Scan(&records) if result.Error != nil { return nil, fmt.Errorf("failed to get applied migrations: %v", result.Error) } for _, record := range records { migrations = append(migrations, record.Name) } return migrations, nil } // getMigrationFiles gets all migration files sorted by filename func getMigrationFiles(migrationsPath string) ([]string, error) { files, err := filepath.Glob(filepath.Join(migrationsPath, "*.sql")) if err != nil { return nil, fmt.Errorf("failed to read migration files: %v", err) } // Check if directory exists if _, err := os.Stat(migrationsPath); os.IsNotExist(err) { return []string{}, nil } sort.Strings(files) return files, nil } // contains checks if a string is in a slice func contains(slice []string, item string) bool { for _, s := range slice { if s == item { return true } } return false }