Files
go-admin/cmd/migrate/server.go
T
zhangwenjian d7a8e66753 feat: add migrate status, --dry-run and --app
--app rejects a code nothing was registered under, on all three paths. It used
to take a typo as "nothing matched" and report success: migrate said the app
was unknown and still exited 0, while --dry-run and status printed the same
words an up-to-date database produces.
2026-09-01 17:45:40 +08:00

252 lines
7.0 KiB
Go

package migrate
import (
"bytes"
"fmt"
"os"
"strconv"
"strings"
"text/template"
"time"
"github.com/go-admin-team/go-admin-core/v2/sdk"
"github.com/go-admin-team/go-admin-core/v2/sdk/pkg"
"gorm.io/gorm"
"github.com/go-admin-team/go-admin-core/v2/config/source/file"
"github.com/spf13/cobra"
"github.com/go-admin-team/go-admin-core/v2/sdk/config"
"go-admin/cmd/migrate/migration"
_ "go-admin/cmd/migrate/migration/version"
_ "go-admin/cmd/migrate/migration/version-local"
"go-admin/common/database"
"go-admin/common/models"
)
var (
configYml string
generate bool
goAdmin bool
host string
appCode string
dryRun bool
StartCmd = &cobra.Command{
Use: "migrate",
Short: "Initialize the database",
Example: "go-admin migrate -c config/settings.yml",
Run: func(cmd *cobra.Command, args []string) {
run()
},
}
statusCmd = &cobra.Command{
Use: "status",
Short: "List applied and pending migrations, grouped by app",
Example: "go-admin migrate status -c config/settings.yml",
Run: func(cmd *cobra.Command, args []string) {
runStatus()
},
}
)
// fixme 在您看不见代码的时候运行迁移,我觉得是不安全的,所以编译后最好不要去执行迁移
func init() {
StartCmd.PersistentFlags().StringVarP(&configYml, "config", "c", "config/settings.yml", "Start server with provided configuration file")
StartCmd.PersistentFlags().BoolVarP(&generate, "generate", "g", false, "generate migration file")
StartCmd.PersistentFlags().BoolVarP(&goAdmin, "goAdmin", "a", false, "with -g, write the generated file to version/ instead of version-local/ (does not affect which migrations run)")
StartCmd.PersistentFlags().StringVarP(&host, "domain", "d", "*", "select tenant host")
// --app is deliberately long-only. -a already means "generate into
// version/ rather than version-local/", which is about writing a template
// file, not about which migrations run; giving the two the same letter
// would be a trap.
StartCmd.PersistentFlags().StringVar(&appCode, "app", "", "limit to the migrations of one app (\""+migration.FrameworkAppCode+"\" for the framework's own)")
StartCmd.Flags().BoolVar(&dryRun, "dry-run", false, "list what would be applied, in order, and write nothing")
StartCmd.AddCommand(statusCmd)
}
func run() {
if !generate {
fmt.Println(`start init`)
//1. 读取配置
config.Setup(
file.NewSource(file.WithPath(configYml)),
initDB,
)
} else {
fmt.Println(`generate migration file`)
_ = genFile()
}
}
// resolveDB picks the tenant database and hands it to the registry.
//
// It creates and alters nothing, which is what lets status and --dry-run share
// it: those two must be able to run against a production database without
// leaving a trace.
func resolveDB() (*gorm.DB, error) {
if host == "" {
host = "*"
}
db := sdk.Runtime.GetDbByTenant(host)
if db == nil {
if len(sdk.Runtime.GetAllDb()) == 1 && host == "*" {
for k, v := range sdk.Runtime.GetAllDb() {
db = v
host = k
break
}
}
}
if db == nil {
return nil, fmt.Errorf("未找到数据库配置")
}
if config.DatabasesConfig[host].Driver == "mysql" {
//初始化数据库时候用
db.Set("gorm:table_options", "ENGINE=InnoDB CHARSET=utf8mb4")
}
return db, nil
}
// exitUnlessAppRegistered ends the command when --app names something no
// migration was registered under.
//
// Every path took a typo as "nothing matched" and reported success: `migrate`
// printed that the app was unknown and still exited 0, while `--dry-run` and
// `status` said "nothing to apply" and "none recorded" - which is what an
// up-to-date database says too, so the output does not even hint at the typo.
// An operator running `go-admin migrate --app crmm && deploy` gets the deploy.
//
// Checked against the registry, which init() has already filled, so this runs
// before any database work and costs nothing. It lives in the command layer
// because the exit code does: the migration package stays callable from a test
// without taking the process down with it.
func exitUnlessAppRegistered() {
if err := appRegistrationError(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
// appRegistrationError carries the decision on its own so it can be tested;
// exitUnlessAppRegistered is only the os.Exit around it. Nil means --app was
// either empty or names a registered app.
func appRegistrationError() error {
if appCode == "" {
return nil
}
want := migration.DisplayAppCode(migration.AppFilter(appCode))
registered := migration.Migrate.AppCodes()
for _, c := range registered {
if c == want {
return nil
}
}
return fmt.Errorf("no migrations are registered for app %q; registered: %s",
want, strings.Join(registered, ", "))
}
func migrateModel() error {
db, err := resolveDB()
if err != nil {
return err
}
// sys_migration is the one table that never goes through a versioned
// migration - it is the table that records them. AutoMigrate realigns it
// on every run, which is how the app_code column reaches an existing
// database without anyone writing a migration for it.
if err = db.Debug().AutoMigrate(&models.Migration{}); err != nil {
return err
}
migration.Migrate.SetDb(db.Debug())
if appCode != "" {
migration.Migrate.MigrateApp(appCode)
return nil
}
migration.Migrate.Migrate()
return nil
}
func initDB() {
// Before the database is touched, so a typo cannot get as far as looking
// like a successful no-op on either path below.
exitUnlessAppRegistered()
//3. 初始化数据库链接
database.Setup()
if dryRun {
db, err := resolveDB()
if err != nil {
fmt.Println(err)
return
}
migration.Migrate.SetDb(db)
entries, err := migration.Migrate.Status()
if err != nil {
fmt.Println(err)
return
}
if err = printPending(os.Stdout, entries, appCode); err != nil {
fmt.Println(err)
}
return
}
//4. 数据库迁移
fmt.Println("数据库迁移开始")
if err := migrateModel(); err != nil {
fmt.Println(err)
return
}
fmt.Println(`数据库基础数据初始化成功`)
}
func runStatus() {
config.Setup(
file.NewSource(file.WithPath(configYml)),
func() {
exitUnlessAppRegistered()
database.Setup()
db, err := resolveDB()
if err != nil {
fmt.Println(err)
return
}
migration.Migrate.SetDb(db)
entries, err := migration.Migrate.Status()
if err != nil {
fmt.Println(err)
return
}
if err = printStatus(os.Stdout, entries, appCode); err != nil {
fmt.Println(err)
}
},
)
}
func genFile() error {
t1, err := template.ParseFiles("template/migrate.template")
if err != nil {
return err
}
m := map[string]string{}
m["GenerateTime"] = strconv.FormatInt(time.Now().UnixNano()/1e6, 10)
m["Package"] = "version_local"
if goAdmin {
m["Package"] = "version"
}
var b1 bytes.Buffer
err = t1.Execute(&b1, m)
if goAdmin {
pkg.FileCreate(b1, "./cmd/migrate/migration/version/"+m["GenerateTime"]+"_migrate.go")
} else {
pkg.FileCreate(b1, "./cmd/migrate/migration/version-local/"+m["GenerateTime"]+"_migrate.go")
}
return nil
}