122 lines
3.5 KiB
Go
122 lines
3.5 KiB
Go
// Package migration owns the application database migration runner. Each data
|
|
// module declares its own steps; the root data package only orders and runs
|
|
// them.
|
|
package migration
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const TableName = "sys_schema_migrations"
|
|
|
|
type Step struct {
|
|
ID string
|
|
Migrate func(*gorm.DB) error
|
|
}
|
|
|
|
type record struct {
|
|
ID string `gorm:"primaryKey;size:255"`
|
|
}
|
|
|
|
func (record) TableName() string { return TableName }
|
|
|
|
func Run(db *gorm.DB, steps []Step) error {
|
|
if db == nil {
|
|
return fmt.Errorf("database is nil")
|
|
}
|
|
db = db.Session(&gorm.Session{NewDB: true})
|
|
if err := repairMigrationTable(db); err != nil {
|
|
return err
|
|
}
|
|
if !db.Session(&gorm.Session{NewDB: true}).Migrator().HasTable(&record{}) {
|
|
if err := db.Session(&gorm.Session{NewDB: true}).Migrator().CreateTable(&record{}); err != nil {
|
|
return fmt.Errorf("create migration table: %w", err)
|
|
}
|
|
}
|
|
var appliedRows []record
|
|
if err := db.Session(&gorm.Session{NewDB: true}).Find(&appliedRows).Error; err != nil {
|
|
return fmt.Errorf("load applied migrations: %w", err)
|
|
}
|
|
applied := make(map[string]struct{}, len(appliedRows))
|
|
known := make(map[string]struct{}, len(steps))
|
|
for _, row := range appliedRows {
|
|
applied[row.ID] = struct{}{}
|
|
}
|
|
for _, step := range steps {
|
|
if step.ID == "" {
|
|
return fmt.Errorf("migration id is empty")
|
|
}
|
|
if _, duplicate := known[step.ID]; duplicate {
|
|
return fmt.Errorf("duplicate migration id %q", step.ID)
|
|
}
|
|
known[step.ID] = struct{}{}
|
|
}
|
|
for id := range applied {
|
|
if _, ok := known[id]; !ok {
|
|
return fmt.Errorf("unknown applied migration %q", id)
|
|
}
|
|
}
|
|
for _, step := range steps {
|
|
if _, ok := applied[step.ID]; ok {
|
|
continue
|
|
}
|
|
if step.Migrate == nil {
|
|
return fmt.Errorf("migration %q has no function", step.ID)
|
|
}
|
|
if err := step.Migrate(db.Session(&gorm.Session{NewDB: true})); err != nil {
|
|
return fmt.Errorf("apply migration %q: %w", step.ID, err)
|
|
}
|
|
if err := db.Session(&gorm.Session{NewDB: true}).Create(&record{ID: step.ID}).Error; err != nil {
|
|
return fmt.Errorf("record migration %q: %w", step.ID, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func repairMigrationTable(db *gorm.DB) error {
|
|
clean := db.Session(&gorm.Session{NewDB: true})
|
|
if !clean.Migrator().HasTable(TableName) {
|
|
return nil
|
|
}
|
|
var dataType string
|
|
switch clean.Dialector.Name() {
|
|
case "mysql":
|
|
err := clean.Raw("SELECT DATA_TYPE FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ? AND column_name = ?", TableName, "id").Scan(&dataType).Error
|
|
if err != nil {
|
|
return fmt.Errorf("inspect migration table: %w", err)
|
|
}
|
|
case "postgres":
|
|
err := clean.Raw("SELECT data_type FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = ? AND column_name = ?", TableName, "id").Scan(&dataType).Error
|
|
if err != nil {
|
|
return fmt.Errorf("inspect migration table: %w", err)
|
|
}
|
|
case "sqlite":
|
|
var rows []struct {
|
|
Name string `gorm:"column:name"`
|
|
Type string `gorm:"column:type"`
|
|
}
|
|
if err := clean.Raw("PRAGMA table_info(" + TableName + ")").Scan(&rows).Error; err != nil {
|
|
return fmt.Errorf("inspect migration table: %w", err)
|
|
}
|
|
for _, row := range rows {
|
|
if strings.EqualFold(row.Name, "id") {
|
|
dataType = row.Type
|
|
break
|
|
}
|
|
}
|
|
default:
|
|
return nil
|
|
}
|
|
normalized := strings.ToLower(dataType)
|
|
if strings.Contains(normalized, "char") || strings.Contains(normalized, "text") || strings.Contains(normalized, "clob") {
|
|
return nil
|
|
}
|
|
if err := clean.Migrator().DropTable(TableName); err != nil {
|
|
return fmt.Errorf("repair invalid migration table: %w", err)
|
|
}
|
|
return nil
|
|
}
|