126 lines
3.9 KiB
Go
126 lines
3.9 KiB
Go
package data
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
drivermysql "github.com/go-sql-driver/mysql"
|
|
"gorm.io/gorm"
|
|
"kra/internal/config"
|
|
"kra/internal/data/swmigration"
|
|
"kra/pkg/database/migration"
|
|
"kra/pkg/module"
|
|
)
|
|
|
|
// MigrateDatabase opens only the configured primary database. It does not
|
|
// create a database, start a scheduler, open SDK clients, or initialize users.
|
|
func MigrateDatabase(ctx context.Context, c *config.Database, catalog module.Catalog, logger *slog.Logger, checkOnly bool, configPath ...string) error {
|
|
if !databaseConnectionConfigured(c) {
|
|
return fmt.Errorf("migration requires an existing configured database")
|
|
}
|
|
migrationConfig := *c
|
|
// One connection holds the advisory lock, a second executes migrations.
|
|
if !checkOnly && migrationConfig.MaxOpenConns > 0 && migrationConfig.MaxOpenConns < 2 {
|
|
migrationConfig.MaxOpenConns = 2
|
|
}
|
|
var db *gorm.DB
|
|
var err error
|
|
if normalizedDriver(c.Driver) == "mysql" && !checkOnly {
|
|
var dsn string
|
|
dsn, err = databaseDSN(&migrationConfig, "")
|
|
if err == nil {
|
|
var options *drivermysql.Config
|
|
options, err = drivermysql.ParseDSN(dsn)
|
|
if err == nil {
|
|
options.ReadTimeout = 0
|
|
options.WriteTimeout = 0
|
|
db, err = openWithDriverConfig("mysql", options.FormatDSN(), &migrationConfig, logger)
|
|
}
|
|
}
|
|
} else {
|
|
db, err = openDatabase(&migrationConfig, false, "", logger)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("open migration database: %w", err)
|
|
}
|
|
defer closeGormDB(db)
|
|
db = db.WithContext(ctx)
|
|
if checkOnly {
|
|
return checkMigratedSchema(db, catalog)
|
|
}
|
|
automatic, err := swmigration.Recognized(db)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if automatic {
|
|
root := ""
|
|
if len(configPath) > 0 && configPath[0] != "" {
|
|
root = filepath.Join(filepath.Dir(configPath[0]), "migration-backups")
|
|
}
|
|
return swmigration.Run(ctx, db, catalog, root, logger)
|
|
}
|
|
// A dedicated connection owns the lock even when a migration uses a
|
|
// transaction or temporarily changes connection-local SQL compatibility.
|
|
run := func() error {
|
|
if err := catalog.CheckPrerequisites(db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrateAll(db, catalog); err != nil {
|
|
return err
|
|
}
|
|
return checkMigratedSchema(db, catalog)
|
|
}
|
|
if db.Dialector.Name() != "mysql" {
|
|
return run()
|
|
}
|
|
return db.Connection(func(conn *gorm.DB) error {
|
|
var name string
|
|
if err := conn.Raw("SELECT CONCAT('sw_to_kra:', MD5(DATABASE()))").Scan(&name).Error; err != nil {
|
|
return err
|
|
}
|
|
var locked int
|
|
if err := conn.Session(&gorm.Session{NewDB: true}).Raw("SELECT GET_LOCK(?, 0)", name).Scan(&locked).Error; err != nil {
|
|
return err
|
|
}
|
|
if locked != 1 {
|
|
return fmt.Errorf("another migration owns the database lock")
|
|
}
|
|
defer func() {
|
|
cleanup, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
|
|
defer cancel()
|
|
conn.Session(&gorm.Session{NewDB: true}).WithContext(cleanup).Exec("DO RELEASE_LOCK(?)", name)
|
|
}()
|
|
return run()
|
|
})
|
|
}
|
|
|
|
// SW upgrades are automatic even for installations that previously disabled
|
|
// generic table auto-migration. Other template databases keep that setting.
|
|
func prepareConfiguredDatabase(ctx context.Context, db *gorm.DB, c *config.Database, catalog module.Catalog, logger *slog.Logger, configPath string, disable bool) error {
|
|
automatic, err := swmigration.Recognized(db.Session(&gorm.Session{NewDB: true}).WithContext(ctx))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if automatic {
|
|
return MigrateDatabase(ctx, c, catalog, logger, false, configPath)
|
|
}
|
|
if disable {
|
|
return checkMigratedSchema(db, catalog)
|
|
}
|
|
return migrateAll(db, catalog)
|
|
}
|
|
|
|
func checkMigratedSchema(db *gorm.DB, catalog module.Catalog) error {
|
|
db = db.Session(&gorm.Session{NewDB: true})
|
|
if err := migration.CheckApplied(db, catalog.MigrationSteps()); err != nil {
|
|
return fmt.Errorf("run -migrate before starting the service: %w", err)
|
|
}
|
|
if err := catalog.CheckSchema(db); err != nil {
|
|
return fmt.Errorf("database schema is incomplete: %w", err)
|
|
}
|
|
return nil
|
|
}
|