spark-server/cmd/migrate/main.go
zhr ae9a71034d feat: implement Spark IM V3 modular monolith
Add HTTP /v1 MVC, TCP send path, groups with server-side member-avatar collage, object-storage upload, and friends/favorites around the architecture-v3 layout.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-17 19:52:11 +08:00

130 lines
2.9 KiB
Go

package main
import (
"context"
"errors"
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"time"
mysqldriver "github.com/go-sql-driver/mysql"
"go.uber.org/zap"
"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 {
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
1060, // duplicate column
1061: // duplicate key name
return true
default:
return false
}
}