memos/store/migrator.go
2024-08-16 08:07:30 +08:00

198 lines
5.9 KiB
Go

package store
import (
"context"
"embed"
"fmt"
"io/fs"
"log/slog"
"regexp"
"sort"
"github.com/pkg/errors"
"github.com/usememos/memos/server/version"
)
//go:embed migration
var migrationFS embed.FS
//go:embed seed
var seedFS embed.FS
// Migrate applies the latest schema to the database.
func (s *Store) Migrate(ctx context.Context) error {
if err := s.preMigrate(ctx); err != nil {
return errors.Wrap(err, "failed to pre-migrate")
}
if s.Profile.Mode == "prod" {
migrationHistoryList, err := s.driver.FindMigrationHistoryList(ctx, &FindMigrationHistory{})
if err != nil {
return errors.Wrap(err, "failed to find migration history")
}
if len(migrationHistoryList) == 0 {
return errors.Errorf("no migration history found")
}
migrationHistoryVersions := []string{}
for _, migrationHistory := range migrationHistoryList {
migrationHistoryVersions = append(migrationHistoryVersions, migrationHistory.Version)
}
sort.Sort(version.SortVersion(migrationHistoryVersions))
latestMigrationHistoryVersion := migrationHistoryVersions[len(migrationHistoryVersions)-1]
currentVersion := version.GetCurrentVersion(s.Profile.Mode)
if version.IsVersionGreaterThan(version.GetSchemaVersion(currentVersion), latestMigrationHistoryVersion) {
minorVersionList := s.getMinorVersionList()
fmt.Println("start migration")
for _, minorVersion := range minorVersionList {
normalizedVersion := minorVersion + ".0"
if version.IsVersionGreaterThan(normalizedVersion, latestMigrationHistoryVersion) && version.IsVersionGreaterOrEqualThan(currentVersion, normalizedVersion) {
fmt.Println("applying migration for", normalizedVersion)
if err := s.applyMigrationForMinorVersion(ctx, minorVersion); err != nil {
return errors.Wrap(err, "failed to apply minor version migration")
}
}
}
fmt.Println("end migrate")
}
} else {
// In demo mode, we should seed the database.
if s.Profile.Mode == "demo" {
if err := s.seed(ctx); err != nil {
return errors.Wrap(err, "failed to seed")
}
}
}
return nil
}
func (s *Store) preMigrate(ctx context.Context) error {
migrationHistoryList, err := s.driver.FindMigrationHistoryList(ctx, &FindMigrationHistory{})
// If there is no migration history, we should apply the latest schema.
if err != nil || len(migrationHistoryList) == 0 {
if err != nil {
slog.Error("failed to find migration history", "error", err)
}
fileName := s.getMigrationBasePath() + latestSchemaFileName
bytes, err := migrationFS.ReadFile(fileName)
if err != nil {
return errors.Errorf("failed to read latest schema file: %s", err)
}
if err := s.execute(ctx, string(bytes)); err != nil {
return errors.Errorf("failed to exec SQL file %s, err %s", fileName, err)
}
if _, err := s.driver.UpsertMigrationHistory(ctx, &UpsertMigrationHistory{
Version: version.GetCurrentVersion(s.Profile.Mode),
}); err != nil {
return errors.Wrap(err, "failed to upsert migration history")
}
}
return nil
}
func (s *Store) getMigrationBasePath() string {
mode := "dev"
if s.Profile.Mode == "prod" {
mode = "prod"
}
return fmt.Sprintf("migration/%s/%s/", s.driver.Type(), mode)
}
func (s *Store) getSeedBasePath() string {
return fmt.Sprintf("seed/%s/", s.driver.Type())
}
const (
latestSchemaFileName = "LATEST__SCHEMA.sql"
)
func (s *Store) applyMigrationForMinorVersion(ctx context.Context, minorVersion string) error {
filenames, err := fs.Glob(migrationFS, fmt.Sprintf("%s%s/*.sql", s.getMigrationBasePath(), minorVersion))
if err != nil {
return errors.Wrap(err, "failed to read migration files")
}
sort.Strings(filenames)
migrationStmt := ""
// Loop over all migration files and execute them in order.
for _, filename := range filenames {
buf, err := migrationFS.ReadFile(filename)
if err != nil {
return errors.Wrapf(err, "failed to read minor version migration file, filename=%s", filename)
}
stmt := string(buf)
migrationStmt += stmt
if err := s.execute(ctx, stmt); err != nil {
return errors.Wrapf(err, "migrate error: %s", stmt)
}
}
// Upsert the newest version to migration_history.
version := minorVersion + ".0"
if _, err = s.driver.UpsertMigrationHistory(ctx, &UpsertMigrationHistory{
Version: version,
}); err != nil {
return errors.Wrapf(err, "failed to upsert migration history with version: %s", version)
}
return nil
}
func (s *Store) seed(ctx context.Context) error {
// Only seed for SQLite.
if s.driver.Type() != "sqlite" {
return nil
}
filenames, err := fs.Glob(seedFS, fmt.Sprintf("%s*.sql", s.getSeedBasePath()))
if err != nil {
return errors.Wrap(err, "failed to read seed files")
}
sort.Strings(filenames)
// Loop over all seed files and execute them in order.
for _, filename := range filenames {
bytes, err := seedFS.ReadFile(filename)
if err != nil {
return errors.Wrapf(err, "failed to read seed file, filename=%s", filename)
}
if err := s.execute(ctx, string(bytes)); err != nil {
return errors.Wrapf(err, "seed error: %s", filename)
}
}
return nil
}
// execute runs a single SQL statement within a transaction.
func (s *Store) execute(ctx context.Context, stmt string) error {
tx, err := s.driver.GetDB().Begin()
if err != nil {
return err
}
defer tx.Rollback()
if _, err := tx.ExecContext(ctx, stmt); err != nil {
return errors.Wrap(err, "failed to execute statement")
}
return tx.Commit()
}
func (s *Store) getMinorVersionList() []string {
var minorDirRegexp = regexp.MustCompile(fmt.Sprintf(`^%s[0-9]+\.[0-9]+$`, s.getMigrationBasePath()))
minorVersionList := []string{}
if err := fs.WalkDir(migrationFS, "migration", func(path string, file fs.DirEntry, err error) error {
if err != nil {
return err
}
if file.IsDir() && minorDirRegexp.MatchString(path) {
minorVersionList = append(minorVersionList, file.Name())
}
return nil
}); err != nil {
panic(err)
}
sort.Sort(version.SortVersion(minorVersionList))
return minorVersionList
}