mirror of
https://github.com/go-admin-team/go-admin.git
synced 2026-09-22 10:33:13 +00:00
322 lines
10 KiB
Go
322 lines
10 KiB
Go
package migration
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
common "go-admin/common/models"
|
|
)
|
|
|
|
var Migrate = newMigration()
|
|
|
|
func newMigration() *Migration {
|
|
return &Migration{version: make(map[string]versionEntry)}
|
|
}
|
|
|
|
// versionEntry is one registered migration plus the app it belongs to. The
|
|
// empty app code means the framework itself, which is also what the
|
|
// sys_migration.app_code column defaults to, so history written before this
|
|
// field existed reads back correctly with no backfill.
|
|
type versionEntry struct {
|
|
appCode string
|
|
fn func(db *gorm.DB, version string) error
|
|
}
|
|
|
|
type Migration struct {
|
|
db *gorm.DB
|
|
version map[string]versionEntry
|
|
mutex sync.Mutex
|
|
}
|
|
|
|
func (e *Migration) GetDb() *gorm.DB {
|
|
return e.db
|
|
}
|
|
|
|
func (e *Migration) SetDb(db *gorm.DB) {
|
|
e.db = db
|
|
}
|
|
|
|
// SetVersion registers a migration owned by the framework. Signature and
|
|
// behaviour are unchanged: every existing call site in version/*.go keeps
|
|
// compiling and keeps writing common.Migration{Version: version} with no app
|
|
// code, which is the correct meaning of "framework".
|
|
func (e *Migration) SetVersion(k string, f func(db *gorm.DB, version string) error) {
|
|
e.setVersion(k, "", f)
|
|
}
|
|
|
|
func (e *Migration) setVersion(k, appCode string, f func(db *gorm.DB, version string) error) {
|
|
e.mutex.Lock()
|
|
defer e.mutex.Unlock()
|
|
e.version[k] = versionEntry{appCode: appCode, fn: f}
|
|
}
|
|
|
|
// AppMigrationFunc is the signature of a migration registered through ForApp.
|
|
//
|
|
// It receives appCode explicitly because the migration - not the framework -
|
|
// writes its own completion row, normally as the last statement inside its own
|
|
// transaction. That is what makes "the schema change and the record of it
|
|
// commit together" true, and the framework cannot insert the row on the
|
|
// migration's behalf without giving that up. Handing the code to the function
|
|
// is what stops an app's migrations from silently recording themselves as the
|
|
// framework's.
|
|
type AppMigrationFunc func(db *gorm.DB, version, appCode string) error
|
|
|
|
// AppRegistrar is a per-app view over a registry.
|
|
type AppRegistrar struct {
|
|
m *Migration
|
|
appCode string
|
|
}
|
|
|
|
// FrameworkAppCode is the name migrate status prints for migrations that belong
|
|
// to the framework rather than to an app, and the name --app accepts to select
|
|
// them. The stored app code for those is the empty string; this is only the
|
|
// spelling humans use. It is reserved - ForApp rejects it - so that every group
|
|
// heading status prints is also a value --app understands.
|
|
const FrameworkAppCode = "core"
|
|
|
|
// ForApp returns a registrar that records migrations under code.
|
|
//
|
|
// The code is lower-cased: sys_migration.version sorts as ASCII, so mixed case
|
|
// would order MyApp before crm for no reason a reader could guess, and the two
|
|
// spellings would group as two different apps in migrate status.
|
|
//
|
|
// An empty or reserved code panics rather than falling back to the framework.
|
|
// Registration happens in init(), so this fires the first time the binary runs
|
|
// anywhere, which is the point: an app whose migrations quietly file themselves
|
|
// under the framework is exactly the class of silent failure this work is meant
|
|
// to remove. Framework migrations call Migrate.SetVersion directly.
|
|
func ForApp(code string) *AppRegistrar { return Migrate.ForApp(code) }
|
|
|
|
// ForApp is the same on an explicit registry, which is what tests use.
|
|
func (e *Migration) ForApp(code string) *AppRegistrar {
|
|
code = NormalizeAppCode(code)
|
|
switch code {
|
|
case "":
|
|
panic("migration.ForApp: empty app code; framework migrations use Migrate.SetVersion")
|
|
case FrameworkAppCode:
|
|
panic("migration.ForApp: app code " + FrameworkAppCode + " is reserved for the framework")
|
|
}
|
|
return &AppRegistrar{m: e, appCode: code}
|
|
}
|
|
|
|
// AppCode reports the code this registrar files migrations under, after
|
|
// normalisation.
|
|
func (r *AppRegistrar) AppCode() string { return r.appCode }
|
|
|
|
// SetVersion registers an app-owned migration under k, which is the bare
|
|
// timestamp taken from the file name exactly as framework migrations do.
|
|
//
|
|
// What reaches sys_migration.version is the namespaced form; the version string
|
|
// handed to f is that same namespaced string, so a migration that writes
|
|
// common.Migration{Version: version, AppCode: appCode} records the key the
|
|
// registry will look for next time.
|
|
func (r *AppRegistrar) SetVersion(k string, f AppMigrationFunc) {
|
|
key := namespacedKey(r.appCode, k)
|
|
r.m.setVersion(key, r.appCode, func(db *gorm.DB, version string) error {
|
|
return f(db, version, r.appCode)
|
|
})
|
|
}
|
|
|
|
// namespacedKey scopes k to appCode so two apps cannot collide on the
|
|
// sys_migration.version primary key by minting the same millisecond timestamp.
|
|
// Framework migrations (appCode == "") stay bare, matching every version string
|
|
// already in production.
|
|
func namespacedKey(appCode, k string) string {
|
|
if appCode == "" {
|
|
return k
|
|
}
|
|
return appCode + "-" + k
|
|
}
|
|
|
|
// StatusEntry is one row of migrate status.
|
|
type StatusEntry struct {
|
|
AppCode string
|
|
Version string
|
|
Registered bool
|
|
Applied bool
|
|
ApplyTime *time.Time
|
|
}
|
|
|
|
// Status merges the in-process registry with sys_migration, so it reports all
|
|
// three shapes at once: registered but not applied, registered and applied, and
|
|
// applied while nothing registers it any more - a row left behind by a
|
|
// migration file that was deleted, or by an app that was uninstalled.
|
|
//
|
|
// It only reads. Nothing here creates or alters a table, which is what lets
|
|
// both `status` and `--dry-run` run against a database without touching it.
|
|
func (e *Migration) Status() ([]StatusEntry, error) {
|
|
if e.db == nil {
|
|
return nil, fmt.Errorf("migration: no database configured")
|
|
}
|
|
|
|
e.mutex.Lock()
|
|
registered := make(map[string]string, len(e.version))
|
|
for k, v := range e.version {
|
|
registered[k] = v.appCode
|
|
}
|
|
e.mutex.Unlock()
|
|
|
|
applied := make(map[string]common.Migration)
|
|
// A database that has never been migrated has no sys_migration table.
|
|
// Reporting everything as pending is the honest answer there; erroring out
|
|
// would make status useless in exactly the case it is most wanted.
|
|
if e.db.Migrator().HasTable(&common.Migration{}) {
|
|
var rows []common.Migration
|
|
if err := e.db.Find(&rows).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
for _, r := range rows {
|
|
applied[r.Version] = r
|
|
}
|
|
}
|
|
|
|
versions := make(map[string]struct{}, len(registered)+len(applied))
|
|
for k := range registered {
|
|
versions[k] = struct{}{}
|
|
}
|
|
for k := range applied {
|
|
versions[k] = struct{}{}
|
|
}
|
|
list := make([]string, 0, len(versions))
|
|
for k := range versions {
|
|
list = append(list, k)
|
|
}
|
|
sort.Strings(list)
|
|
|
|
out := make([]StatusEntry, 0, len(list))
|
|
for _, v := range list {
|
|
entry := StatusEntry{Version: v}
|
|
if code, ok := registered[v]; ok {
|
|
entry.Registered = true
|
|
entry.AppCode = code
|
|
}
|
|
if row, ok := applied[v]; ok {
|
|
entry.Applied = true
|
|
t := row.ApplyTime
|
|
entry.ApplyTime = &t
|
|
if !entry.Registered {
|
|
// Nothing registers this version any more, so the database is
|
|
// the only source left for what it belonged to.
|
|
entry.AppCode = row.AppCode
|
|
}
|
|
}
|
|
out = append(out, entry)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// Migrate applies every registered migration that has not been applied yet,
|
|
// across all apps. Existing callers are unaffected.
|
|
func (e *Migration) Migrate() { e.run(allApps) }
|
|
|
|
// MigrateApp applies only the migrations registered under appCode. Pass
|
|
// FrameworkAppCode for the framework's own migrations.
|
|
func (e *Migration) MigrateApp(appCode string) { e.run(AppFilter(appCode)) }
|
|
|
|
// NormalizeAppCode applies the same rule ForApp does, so a code typed on the
|
|
// command line matches one written in an init().
|
|
func NormalizeAppCode(code string) string {
|
|
return strings.ToLower(strings.TrimSpace(code))
|
|
}
|
|
|
|
// AppFilter turns a code as typed into the code stored in the registry, so
|
|
// "core" selects the framework's migrations, whose stored code is empty.
|
|
func AppFilter(code string) string {
|
|
code = NormalizeAppCode(code)
|
|
if code == FrameworkAppCode {
|
|
return ""
|
|
}
|
|
return code
|
|
}
|
|
|
|
// DisplayAppCode is the inverse: what to print for a stored code.
|
|
func DisplayAppCode(code string) string {
|
|
if code == "" {
|
|
return FrameworkAppCode
|
|
}
|
|
return code
|
|
}
|
|
|
|
// AppCodes lists the app codes with at least one registered migration, framework
|
|
// included under its display name, sorted.
|
|
func (e *Migration) AppCodes() []string {
|
|
e.mutex.Lock()
|
|
seen := map[string]struct{}{}
|
|
for _, v := range e.version {
|
|
seen[DisplayAppCode(v.appCode)] = struct{}{}
|
|
}
|
|
e.mutex.Unlock()
|
|
|
|
out := make([]string, 0, len(seen))
|
|
for code := range seen {
|
|
out = append(out, code)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
func (e *Migration) run(appCode string) {
|
|
e.mutex.Lock()
|
|
versions := make([]string, 0, len(e.version))
|
|
entries := make(map[string]versionEntry, len(e.version))
|
|
for k, v := range e.version {
|
|
if appCode != allApps && v.appCode != appCode {
|
|
continue
|
|
}
|
|
versions = append(versions, k)
|
|
entries[k] = v
|
|
}
|
|
e.mutex.Unlock()
|
|
sort.Strings(versions)
|
|
|
|
// A mistyped --app would otherwise select nothing and report "no
|
|
// migrations to apply", which reads exactly like "already up to date".
|
|
if appCode != allApps && len(versions) == 0 {
|
|
log.Printf("no migrations are registered for app %q; registered: %s",
|
|
DisplayAppCode(appCode), strings.Join(e.AppCodes(), ", "))
|
|
return
|
|
}
|
|
|
|
var err error
|
|
var count int64
|
|
applied := 0
|
|
for _, v := range versions {
|
|
err = e.db.Table("sys_migration").Where("version = ?", v).Count(&count).Error
|
|
if err != nil {
|
|
log.Fatalln(err)
|
|
}
|
|
if count > 0 {
|
|
// Already applied. This used to print the bare count, so a mature
|
|
// database wrote a screen of "1" at every start.
|
|
count = 0
|
|
continue
|
|
}
|
|
log.Printf("applying migration %s", v)
|
|
if err = entries[v].fn(e.db.Debug(), v); err != nil {
|
|
log.Fatalf("migration %s failed: %v", v, err)
|
|
}
|
|
applied++
|
|
}
|
|
if applied == 0 {
|
|
log.Println("no migrations to apply")
|
|
} else {
|
|
log.Printf("applied %d migration(s)", applied)
|
|
}
|
|
}
|
|
|
|
// allApps is the sentinel run() takes to mean "do not filter". It is distinct
|
|
// from the empty app code, which selects the framework's own migrations.
|
|
const allApps = "\x00all"
|
|
|
|
func GetFilename(s string) string {
|
|
s = filepath.Base(s)
|
|
return s[:13]
|
|
}
|