c65e357e15
Apply BasePath via fs.Sub, route CLI through Provider API, return ErrPendingMigrations when auto-migrate is disabled, use DownTo for blank. Co-authored-by: Cursor <cursoragent@cursor.com>
167 lines
4.5 KiB
Go
167 lines
4.5 KiB
Go
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)
|
|
}
|