118 lines
2.4 KiB
Go
118 lines
2.4 KiB
Go
package db
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
func RunMigrations(db *sql.DB, migrationsDir string) error {
|
|
if migrationsDir == "" {
|
|
return fmt.Errorf("migrations directory is required")
|
|
}
|
|
|
|
// Create migration tracking table
|
|
_, err := db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS migration_version (
|
|
name TEXT PRIMARY KEY,
|
|
applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
`)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
migrations, err := filepath.Glob(filepath.Join(migrationsDir, "*.sql"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(migrations) == 0 {
|
|
return fmt.Errorf("no migration files found in %s", migrationsDir)
|
|
}
|
|
|
|
sort.Strings(migrations)
|
|
|
|
appliedNames, err := loadAppliedMigrationNames(db)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, migrationFile := range migrations {
|
|
migrationName := filepath.Base(migrationFile)
|
|
if migrationApplied(appliedNames, migrationName) {
|
|
// already applied, skipping silently
|
|
continue
|
|
}
|
|
|
|
// Read and execute migration
|
|
migrationSQL, err := os.ReadFile(migrationFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Strict execution: if it fails, it halts.
|
|
if _, err := db.Exec(string(migrationSQL)); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Mark as applied
|
|
_, err = db.Exec("INSERT INTO migration_version (name) VALUES (?)", migrationName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
appliedNames[migrationName] = struct{}{}
|
|
|
|
log.Printf("migration %s applied successfully", migrationName)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func loadAppliedMigrationNames(db *sql.DB) (map[string]struct{}, error) {
|
|
rows, err := db.Query("SELECT name FROM migration_version")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
applied := make(map[string]struct{})
|
|
for rows.Next() {
|
|
var name string
|
|
if err := rows.Scan(&name); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
applied[name] = struct{}{}
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return applied, nil
|
|
}
|
|
|
|
func migrationApplied(appliedNames map[string]struct{}, migrationName string) bool {
|
|
if _, exists := appliedNames[migrationName]; exists {
|
|
return true
|
|
}
|
|
|
|
legacyName := filepath.ToSlash(filepath.Join("migrations", migrationName))
|
|
if _, exists := appliedNames[legacyName]; exists {
|
|
return true
|
|
}
|
|
|
|
for appliedName := range appliedNames {
|
|
if strings.EqualFold(filepath.Base(appliedName), migrationName) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|