package store import ( "database/sql" "fmt" "io/fs" "log/slog" "sort" "git.yeij.top/AskaEth/Scoreboard/server/migrations" ) const createMigrationsTable = `CREATE TABLE IF NOT EXISTS schema_migrations ( name TEXT PRIMARY KEY, applied_at TEXT NOT NULL DEFAULT (datetime('now')) )` // Migrate 按文件名顺序迁移系统库(data/system.db)。 func (s *Store) Migrate() error { fsys, err := fs.Sub(migrations.SystemFS, "system") if err != nil { return fmt.Errorf("定位系统库迁移目录: %w", err) } return migrate(s.db, fsys, "system") } // MigrateEvent 迁移赛事库(event.db)。赛事库由调用方自行打开, // 本函数只负责执行 event/ 下的迁移,不持有连接。 func MigrateEvent(db *sql.DB) error { fsys, err := fs.Sub(migrations.EventFS, "event") if err != nil { return fmt.Errorf("定位赛事库迁移目录: %w", err) } return migrate(db, fsys, "event") } // migrate 执行 fsys 中尚未应用的迁移,每个迁移在独立事务中完成。 func migrate(db *sql.DB, fsys fs.FS, label string) error { if _, err := db.Exec(createMigrationsTable); err != nil { return fmt.Errorf("创建 schema_migrations: %w", err) } applied, err := appliedMigrations(db) if err != nil { return err } names, err := fs.Glob(fsys, "*.sql") if err != nil { return err } sort.Strings(names) for _, name := range names { if applied[name] { continue } body, err := fs.ReadFile(fsys, name) if err != nil { return fmt.Errorf("读取迁移 %s: %w", name, err) } if err := applyMigration(db, name, string(body)); err != nil { return err } slog.Info("已应用迁移", "db", label, "name", name) } return nil } func appliedMigrations(db *sql.DB) (map[string]bool, error) { rows, err := db.Query(`SELECT name FROM schema_migrations`) if err != nil { return nil, fmt.Errorf("查询迁移记录: %w", err) } defer func() { _ = rows.Close() }() applied := make(map[string]bool) for rows.Next() { var name string if err := rows.Scan(&name); err != nil { return nil, err } applied[name] = true } if err := rows.Err(); err != nil { return nil, err } return applied, nil } func applyMigration(db *sql.DB, name, body string) error { tx, err := db.Begin() if err != nil { return fmt.Errorf("开启事务: %w", err) } defer func() { _ = tx.Rollback() }() if _, err := tx.Exec(body); err != nil { return fmt.Errorf("执行迁移 %s: %w", name, err) } if _, err := tx.Exec(`INSERT INTO schema_migrations(name) VALUES (?)`, name); err != nil { return fmt.Errorf("记录迁移 %s: %w", name, err) } return tx.Commit() }