fix: unify goose Provider path and fail on pending migrations
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>
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user