96 lines
2.3 KiB
Go
96 lines
2.3 KiB
Go
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
"embed"
|
|
"fmt"
|
|
"io/fs"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
//go:embed migrations/*.sql
|
|
var migrationFS embed.FS
|
|
|
|
// Migrate 应用尚未执行的迁移。T0.1 仅为空执行器加 0001 占位;完整备份与表结构见 T0.3。
|
|
func Migrate(db *sql.DB) error {
|
|
if _, err := db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
version INTEGER PRIMARY KEY,
|
|
applied_at INTEGER NOT NULL
|
|
)`); err != nil {
|
|
return fmt.Errorf("ensure schema_migrations: %w", err)
|
|
}
|
|
|
|
entries, err := fs.ReadDir(migrationFS, "migrations")
|
|
if err != nil {
|
|
return fmt.Errorf("read migrations: %w", err)
|
|
}
|
|
var names []string
|
|
for _, e := range entries {
|
|
if e.IsDir() || !strings.HasSuffix(e.Name(), ".sql") {
|
|
continue
|
|
}
|
|
names = append(names, e.Name())
|
|
}
|
|
sort.Strings(names)
|
|
|
|
for _, name := range names {
|
|
version, err := parseMigrationVersion(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var exists int
|
|
err = db.QueryRow(`SELECT 1 FROM schema_migrations WHERE version = ?`, version).Scan(&exists)
|
|
if err == nil {
|
|
continue
|
|
}
|
|
if err != sql.ErrNoRows {
|
|
return fmt.Errorf("check migration %d: %w", version, err)
|
|
}
|
|
|
|
body, err := migrationFS.ReadFile("migrations/" + name)
|
|
if err != nil {
|
|
return fmt.Errorf("read migration %s: %w", name, err)
|
|
}
|
|
sqlText := strings.TrimSpace(string(body))
|
|
tx, err := db.Begin()
|
|
if err != nil {
|
|
return fmt.Errorf("begin migration %d: %w", version, err)
|
|
}
|
|
if sqlText != "" {
|
|
if _, err := tx.Exec(sqlText); err != nil {
|
|
_ = tx.Rollback()
|
|
return fmt.Errorf("apply migration %d: %w", version, err)
|
|
}
|
|
}
|
|
if _, err := tx.Exec(
|
|
`INSERT INTO schema_migrations(version, applied_at) VALUES(?, ?)`,
|
|
version,
|
|
time.Now().UnixMilli(),
|
|
); err != nil {
|
|
_ = tx.Rollback()
|
|
return fmt.Errorf("record migration %d: %w", version, err)
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return fmt.Errorf("commit migration %d: %w", version, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parseMigrationVersion(name string) (int, error) {
|
|
base := strings.TrimSuffix(name, ".sql")
|
|
parts := strings.SplitN(base, "_", 2)
|
|
if len(parts) < 1 {
|
|
return 0, fmt.Errorf("invalid migration name: %s", name)
|
|
}
|
|
v, err := strconv.Atoi(parts[0])
|
|
if err != nil {
|
|
return 0, fmt.Errorf("invalid migration version in %s: %w", name, err)
|
|
}
|
|
return v, nil
|
|
}
|