
This commit adds a complete Go implementation of the Todo application. The application uses Gin framework for the web server, GORM for database access, and SQLite for storage. Key features: - Todo CRUD operations with the same API endpoints - Health check endpoint - Database migrations - Tests for models, services, and API handlers - Documentation for the API - Configurable settings
152 lines
3.6 KiB
Go
152 lines
3.6 KiB
Go
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
|
|
} |