Files
end c65e357e15 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>
2026-06-29 18:04:08 -07:00

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)
}