Auto-apply migrations on startup
This commit is contained in:
112
internal/repository/migrate/apply.go
Normal file
112
internal/repository/migrate/apply.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package migrate
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// EnsureSchema applies all "up" migrations when the database has no tables.
|
||||
func EnsureSchema(db *sql.DB, dir string) error {
|
||||
if err := ensureSchemaMigrations(db); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
empty, err := isDatabaseEmpty(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
runner := Runner{Dir: dir}
|
||||
migrations, err := runner.Load(Up)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if empty {
|
||||
for _, migration := range migrations {
|
||||
if err := execStatements(db, migration.SQL); err != nil {
|
||||
return fmt.Errorf("apply %s: %w", migration.Name, err)
|
||||
}
|
||||
if err := recordMigration(db, migration.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
applied, err := loadAppliedMigrations(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, migration := range migrations {
|
||||
if applied[migration.Name] {
|
||||
continue
|
||||
}
|
||||
if err := execStatements(db, migration.SQL); err != nil {
|
||||
return fmt.Errorf("apply %s: %w", migration.Name, err)
|
||||
}
|
||||
if err := recordMigration(db, migration.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isDatabaseEmpty(db *sql.DB) (bool, error) {
|
||||
var count int
|
||||
if err := db.QueryRow(`SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE()`).Scan(&count); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count == 0, nil
|
||||
}
|
||||
|
||||
func execStatements(db *sql.DB, sqlText string) error {
|
||||
statements := strings.Split(sqlText, ";")
|
||||
for _, stmt := range statements {
|
||||
stmt = strings.TrimSpace(stmt)
|
||||
if stmt == "" {
|
||||
continue
|
||||
}
|
||||
if _, err := db.Exec(stmt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureSchemaMigrations(db *sql.DB) error {
|
||||
_, err := db.Exec(`CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
name VARCHAR(255) NOT NULL UNIQUE,
|
||||
applied_at TIMESTAMP NOT NULL
|
||||
)`)
|
||||
return err
|
||||
}
|
||||
|
||||
func loadAppliedMigrations(db *sql.DB) (map[string]bool, error) {
|
||||
rows, err := db.Query(`SELECT name FROM schema_migrations`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
applied := map[string]bool{}
|
||||
for rows.Next() {
|
||||
var name string
|
||||
if err := rows.Scan(&name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
applied[name] = true
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return applied, nil
|
||||
}
|
||||
|
||||
func recordMigration(db *sql.DB, name string) error {
|
||||
_, err := db.Exec(`INSERT INTO schema_migrations (name, applied_at) VALUES (?, ?)`, name, time.Now().UTC())
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user