Files
kra-new/pkg/database/migration/check.go
T

160 lines
4.5 KiB
Go

package migration
import (
"fmt"
"sort"
"strings"
"gorm.io/gorm"
)
// CheckApplied is read-only; a missing or corrupt ledger must not be repaired
// by a normal service startup with automatic migrations disabled.
func CheckApplied(db *gorm.DB, steps []Step) error {
db = db.Session(&gorm.Session{NewDB: true})
var rows []record
if err := db.Model(&record{}).Find(&rows).Error; err != nil {
return fmt.Errorf("read migration versions: %w", err)
}
known := map[string]bool{}
for _, step := range steps {
if step.ID == "" || known[step.ID] {
return fmt.Errorf("invalid or duplicate migration ID %q", step.ID)
}
known[step.ID] = true
}
for _, row := range rows {
if _, ok := known[row.ID]; !ok {
return fmt.Errorf("unknown applied migration %q", row.ID)
}
known[row.ID] = false
}
var pending []string
for id, missing := range known {
if missing {
pending = append(pending, id)
}
}
sort.Strings(pending)
if len(pending) > 0 {
return fmt.Errorf("pending migrations: %s", strings.Join(pending, ", "))
}
return nil
}
// CheckModels verifies persistent columns and declared indexes without
// changing data or schema. Extra legacy columns and indexes are retained.
func CheckModels(db *gorm.DB, models ...any) error {
db = db.Session(&gorm.Session{NewDB: true})
var problems []string
for _, model := range models {
stmt := &gorm.Statement{DB: db}
if err := stmt.Parse(model); err != nil {
return err
}
table := stmt.Schema.Table
if !db.Migrator().HasTable(model) {
problems = append(problems, "missing table "+table)
continue
}
columns, err := db.Migrator().ColumnTypes(model)
if err != nil {
return err
}
found := map[string]bool{}
for _, column := range columns {
found[column.Name()] = true
}
for _, name := range stmt.Schema.DBNames {
if !stmt.Schema.FieldsByDBName[name].IgnoreMigration && !found[name] {
problems = append(problems, "missing column "+table+"."+name)
}
}
indexes, err := readIndexes(db, model, table)
if err != nil {
return err
}
if db.Dialector.Name() == "mysql" && len(stmt.Schema.PrimaryFields) > 0 {
var wanted []string
for _, field := range stmt.Schema.PrimaryFields {
wanted = append(wanted, field.DBName)
}
matched := false
for _, actual := range indexes {
if actual.primary && strings.Join(actual.columns, ",") == strings.Join(wanted, ",") {
matched = true
}
}
if !matched {
problems = append(problems, "missing or incompatible primary key "+table)
}
}
for _, index := range stmt.Schema.ParseIndexes() {
wanted := make([]string, 0, len(index.Fields))
for _, field := range index.Fields {
wanted = append(wanted, field.DBName)
}
covered := false
for _, actual := range indexes {
if actual.prefix || len(actual.columns) < len(wanted) {
continue
}
if index.Class == "UNIQUE" && (!actual.unique || len(actual.columns) != len(wanted)) {
continue
}
if strings.Join(actual.columns[:len(wanted)], ",") == strings.Join(wanted, ",") {
covered = true
break
}
}
if !covered {
problems = append(problems, "missing index "+table+"."+index.Name)
}
}
}
if len(problems) > 0 {
return fmt.Errorf("%s", strings.Join(problems, "; "))
}
return nil
}
type schemaIndex struct {
columns []string
unique, prefix, primary bool
}
func readIndexes(db *gorm.DB, model any, table string) ([]schemaIndex, error) {
if db.Dialector.Name() == "mysql" {
var rows []struct {
IndexName, ColumnName string
NonUnique int
SubPart *int
}
if err := db.Raw("SELECT index_name,column_name,non_unique,sub_part FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name=? ORDER BY index_name,seq_in_index", table).Scan(&rows).Error; err != nil {
return nil, err
}
var indexes []schemaIndex
previous := ""
for _, row := range rows {
if row.IndexName != previous {
indexes = append(indexes, schemaIndex{unique: row.NonUnique == 0, primary: row.IndexName == "PRIMARY"})
previous = row.IndexName
}
index := &indexes[len(indexes)-1]
index.columns = append(index.columns, row.ColumnName)
index.prefix = index.prefix || row.SubPart != nil
}
return indexes, nil
}
rows, err := db.Migrator().GetIndexes(model)
if err != nil {
return nil, err
}
indexes := make([]schemaIndex, 0, len(rows))
for _, row := range rows {
unique, _ := row.Unique()
indexes = append(indexes, schemaIndex{columns: row.Columns(), unique: unique})
}
return indexes, nil
}