164 lines
4.4 KiB
Go
164 lines
4.4 KiB
Go
// Package migration 基于 golang-migrate 提供数据库结构迁移能力。
|
|
// 迁移脚本以 go:embed 打包进二进制,通过独立命令 cmd/migrate 手动执行;服务启动流程不触发迁移。
|
|
package migration
|
|
|
|
import (
|
|
"database/sql"
|
|
"embed"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/setting"
|
|
_ "github.com/go-sql-driver/mysql"
|
|
"github.com/golang-migrate/migrate/v4"
|
|
migratemysql "github.com/golang-migrate/migrate/v4/database/mysql"
|
|
"github.com/golang-migrate/migrate/v4/source/iofs"
|
|
)
|
|
|
|
// migrationsDir 迁移脚本所在目录:脚本与 migration.go 同级,故为当前目录。
|
|
const migrationsDir = "."
|
|
|
|
//go:embed *.sql
|
|
var migrationFS embed.FS
|
|
|
|
// dsn 拼装迁移专用 DSN。
|
|
// 必须开启 multiStatements:单个迁移文件内含多条 CREATE TABLE / CREATE INDEX 语句,
|
|
// 未开启时 MySQL 会拒绝执行。该参数与 gorm 使用的 DSN 不同,故此处单独拼装。
|
|
func dsn(s *setting.MysqlSettingS) string {
|
|
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&multiStatements=true",
|
|
s.UserName,
|
|
s.Password,
|
|
s.Host,
|
|
s.Port,
|
|
s.DBName,
|
|
)
|
|
}
|
|
|
|
// newMigrate 创建 migrate 实例;返回的 close 用于释放底层数据库连接。
|
|
func newMigrate(s *setting.MysqlSettingS) (*migrate.Migrate, func() error, error) {
|
|
if s == nil {
|
|
return nil, nil, errors.New("mysql setting is nil")
|
|
}
|
|
if s.DBName == "" {
|
|
return nil, nil, errors.New("mysql DBName is empty")
|
|
}
|
|
|
|
source, err := iofs.New(migrationFS, migrationsDir)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("load embedded migrations: %w", err)
|
|
}
|
|
|
|
db, err := sql.Open("mysql", dsn(s))
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("open mysql: %w", err)
|
|
}
|
|
closeFn := func() error { return db.Close() }
|
|
|
|
driver, err := migratemysql.WithInstance(db, &migratemysql.Config{})
|
|
if err != nil {
|
|
_ = closeFn()
|
|
return nil, nil, fmt.Errorf("create mysql migrate driver: %w", err)
|
|
}
|
|
|
|
m, err := migrate.NewWithInstance("iofs", source, "mysql", driver)
|
|
if err != nil {
|
|
_ = closeFn()
|
|
return nil, nil, fmt.Errorf("create migrate instance: %w", err)
|
|
}
|
|
return m, closeFn, nil
|
|
}
|
|
|
|
// Up 执行到最新版本;无待执行迁移(ErrNoChange)视为成功。
|
|
func Up(s *setting.MysqlSettingS) error {
|
|
m, closeFn, err := newMigrate(s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer closeFn()
|
|
|
|
if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
|
return fmt.Errorf("migrate up: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Down 全量回滚所有已应用的迁移。
|
|
func Down(s *setting.MysqlSettingS) error {
|
|
m, closeFn, err := newMigrate(s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer closeFn()
|
|
|
|
if err := m.Down(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
|
return fmt.Errorf("migrate down: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Steps 按步数迁移:n > 0 向上 n 步,n < 0 向下 n 步。
|
|
func Steps(s *setting.MysqlSettingS, n int) error {
|
|
m, closeFn, err := newMigrate(s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer closeFn()
|
|
|
|
if err := m.Steps(n); err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
|
return fmt.Errorf("migrate steps %d: %w", n, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Version 返回当前迁移版本号、是否为脏库状态。未执行过任何迁移时返回 0。
|
|
func Version(s *setting.MysqlSettingS) (uint, bool, error) {
|
|
m, closeFn, err := newMigrate(s)
|
|
if err != nil {
|
|
return 0, false, err
|
|
}
|
|
defer closeFn()
|
|
|
|
version, dirty, err := m.Version()
|
|
if err != nil {
|
|
if errors.Is(err, migrate.ErrNilVersion) {
|
|
return 0, false, nil
|
|
}
|
|
return 0, false, fmt.Errorf("migrate version: %w", err)
|
|
}
|
|
return version, dirty, nil
|
|
}
|
|
|
|
// Force 强制设置版本号并清除脏标记,用于修复迁移中途失败导致的 dirty 状态。
|
|
func Force(s *setting.MysqlSettingS, version int) error {
|
|
if version < 0 {
|
|
return fmt.Errorf("force version must not be negative: %d", version)
|
|
}
|
|
m, closeFn, err := newMigrate(s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer closeFn()
|
|
|
|
if err := m.Force(version); err != nil {
|
|
return fmt.Errorf("migrate force %d: %w", version, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Run 按 action 执行迁移动作;arg 仅 steps / force 使用。
|
|
// action 取值:up、down、steps、force。
|
|
func Run(s *setting.MysqlSettingS, action string, arg int) error {
|
|
switch action {
|
|
case "up":
|
|
return Up(s)
|
|
case "down":
|
|
return Down(s)
|
|
case "steps":
|
|
return Steps(s, arg)
|
|
case "force":
|
|
return Force(s, arg)
|
|
default:
|
|
return fmt.Errorf("unknown migration action: %s", action)
|
|
}
|
|
}
|