first commit
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user