package main import ( "context" "errors" "flag" "fmt" "os" "path/filepath" "strings" "time" mysqldriver "github.com/go-sql-driver/mysql" "go.uber.org/zap" "gorm.io/gorm" "spark-server/internal/bootstrap" "spark-server/internal/infrastructure/mysql" redisx "spark-server/internal/infrastructure/redis" ) func main() { cfgPath := flag.String("config", "configs/config.yaml", "config path") dir := flag.String("dir", "migrations", "SQL migrations directory") flag.Parse() if err := run(*cfgPath, *dir); err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) os.Exit(1) } } func run(cfgPath, dir string) error { cfg, err := bootstrap.Load(cfgPath) if err != nil { return err } log := zap.NewNop() db, err := mysql.Open(log, &mysql.Options{ DSN: cfg.MySQL.DSN, SlowThreshold: cfg.MySQL.SlowThreshold, MaxOpenConns: cfg.MySQL.MaxOpenConns, MaxIdleConns: cfg.MySQL.MaxIdleConns, ConnMaxLifetime: cfg.MySQL.ConnMaxLifetime, ConnMaxIdleTime: cfg.MySQL.ConnMaxIdleTime, }) if err != nil { return fmt.Errorf("mysql: %w", err) } defer db.Close() if !db.Ready() { return fmt.Errorf("mysql: dsn is empty") } rdb, err := redisx.Open(&redisx.Options{ Addr: cfg.Redis.Addr, Username: cfg.Redis.Username, Password: cfg.Redis.Password, DB: cfg.Redis.DB, }) if err != nil { return fmt.Errorf("redis: %w", err) } defer rdb.Close() if !rdb.Ready() { return fmt.Errorf("redis: addr is empty") } pingCtx, pingCancel := context.WithTimeout(context.Background(), 10*time.Second) defer pingCancel() if err := rdb.Ping(pingCtx); err != nil { return fmt.Errorf("redis ping: %w", err) } files, err := filepath.Glob(filepath.Join(dir, "*.sql")) if err != nil { return err } gdb := db.Gorm() ctx := context.Background() for _, path := range files { if skipFriendDirected(gdb, path) { fmt.Printf("skip %s\n", filepath.Base(path)) continue } raw, err := os.ReadFile(path) if err != nil { return err } for i, stmt := range splitSQL(string(raw)) { if err := gdb.WithContext(ctx).Exec(stmt).Error; err != nil { if ignorableDDL(err) { continue } return fmt.Errorf("%s statement %d: %w", filepath.Base(path), i+1, err) } } fmt.Printf("ok %s\n", filepath.Base(path)) } fmt.Println("mysql ok, redis ok") return nil } func splitSQL(raw string) []string { var b strings.Builder for _, line := range strings.Split(raw, "\n") { trim := strings.TrimSpace(line) if trim == "" || strings.HasPrefix(trim, "--") { continue } b.WriteString(line) b.WriteByte('\n') } parts := strings.Split(b.String(), ";") out := make([]string, 0, len(parts)) for _, p := range parts { s := strings.TrimSpace(p) if s != "" { out = append(out, s) } } return out } func ignorableDDL(err error) bool { var me *mysqldriver.MySQLError if !errors.As(err, &me) { return false } switch me.Number { case 1050, // table already exists 1051, // unknown table 1060, // duplicate column 1061, // duplicate key name 1091, // can't drop; check that column/key exists 1146: // table doesn't exist return true default: return false } } func skipFriendDirected(gdb *gorm.DB, path string) bool { if filepath.Base(path) != "0013_friend_directed.sql" || gdb == nil { return false } var n int64 err := gdb.Raw(`SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'friend_relations' AND column_name = 'user_id'`).Scan(&n).Error return err == nil && n > 0 }