package migrate import ( "context" "database/sql" "errors" "fmt" "io/fs" "path" "gitea.auvem.com/go-toolkit/app" "gitea.auvem.com/go-toolkit/dbx" "github.com/pressly/goose/v3" ) // SQLOFunc returns the SQL database handle for migrations. type SQLOFunc func() *sql.DB var ( ErrNotInitialized = errors.New("migrations module not initialized") ErrPendingMigrations = errors.New("pending migrations detected but auto-migration is disabled") ErrInvalidMigrationCfg = errors.New("invalid migration configuration") ) // MigrationOpts defines options for the migrations module. type MigrationOpts struct { // SQLO is the SQL database handle getter used for migrations. REQUIRED. SQLO SQLOFunc // Dialect is the database dialect used for migrations (e.g., "mysql", "postgres"). // REQUIRED. Must match the dialect used in dbx. Dialect goose.Dialect // FS is the filesystem where migration files are stored. FS fs.FS // BasePath is the directory within FS where migration files are located. // Defaults to "." if not set. BasePath string } const ( ModuleMigrationsName = "migrations" ModuleMigrateUpName = "migrate up" ModuleMigrateBlankName = "migrate down to blank" ModuleAutoMigrateName = "auto migrate" ) var ( migrationProvider *goose.Provider migrationsConfig MigrationOpts migrationsModule *app.Module moduleMigrateBlank *app.Module moduleMigrateUp *app.Module autoMigrateEnabled bool autoMigrateModule *app.Module ) // MigrationsConfig returns a copy of the active migration configuration. func MigrationsConfig() MigrationOpts { migrationsModule.RequireLoaded() return migrationsConfig } // ModuleMigrations returns the migrations module with the provided configuration. func ModuleMigrations(cfg *MigrationOpts) (*app.Module, error) { if migrationsModule != nil { return nil, fmt.Errorf("ModuleMigrations initialized multiple times") } if err := validateMigrationOpts(cfg); err != nil { return nil, err } migrationsConfig = *cfg migrationsModule = app.NewModule(ModuleMigrationsName, app.ModuleOpts{ Setup: setupMigrations, Depends: []string{dbx.ModuleDBName}, }) return migrationsModule, nil } // MustModuleMigrations is like [ModuleMigrations] but panics on error. func MustModuleMigrations(cfg *MigrationOpts) *app.Module { mod, err := ModuleMigrations(cfg) if err != nil { panic(err) } return mod } // ModuleMigrateUp returns a module that applies pending migrations on setup. func ModuleMigrateUp() *app.Module { if moduleMigrateUp != nil { panic("ModuleMigrateUp initialized multiple times") } moduleMigrateUp = app.NewModule(ModuleMigrateUpName, app.ModuleOpts{ Setup: func(_ *app.Module) error { _, err := ApplyPendingMigrations(context.Background(), -1) return err }, Depends: []string{ModuleMigrationsName}, }) return moduleMigrateUp } // ModuleMigrateBlank returns a module that rolls back all migrations. func ModuleMigrateBlank() *app.Module { if moduleMigrateBlank != nil { panic("ModuleMigrateBlank initialized multiple times") } moduleMigrateBlank = app.NewModule(ModuleMigrateBlankName, app.ModuleOpts{ Setup: func(_ *app.Module) error { _, err := MigrateToBlank(context.Background()) return err }, Depends: []string{ModuleMigrationsName}, }) return moduleMigrateBlank } // ModuleAutoMigrate returns a module that auto-applies pending migrations when enabled. func ModuleAutoMigrate(enabled bool) *app.Module { if autoMigrateModule != nil { panic("ModuleAutoMigrate initialized multiple times") } autoMigrateEnabled = enabled autoMigrateModule = app.NewModule(ModuleAutoMigrateName, app.ModuleOpts{ Setup: func(_ *app.Module) error { return AutoMigrate(context.Background()) }, Depends: []string{ModuleMigrationsName}, }) return autoMigrateModule } func validateMigrationOpts(cfg *MigrationOpts) error { if cfg == nil { return fmt.Errorf("%w: config is nil", ErrInvalidMigrationCfg) } if cfg.SQLO == nil { return fmt.Errorf("%w: SQLO is required", ErrInvalidMigrationCfg) } if cfg.FS == nil { return fmt.Errorf("%w: FS is required", ErrInvalidMigrationCfg) } if cfg.Dialect == "" { return fmt.Errorf("%w: Dialect is required", ErrInvalidMigrationCfg) } if cfg.BasePath == "" { cfg.BasePath = "." } return nil } func migrationFS(cfg MigrationOpts) (fs.FS, error) { if cfg.BasePath == "." || cfg.BasePath == "" { return cfg.FS, nil } return fs.Sub(cfg.FS, cfg.BasePath) } func normalizeCreateDir(basePath string) string { if basePath == "." || basePath == "" { return "migrations" } return path.Clean(basePath) }