Extending
The library is designed around interfaces. You can implement custom Sources, Targets, or Planners to support any storage backend or migration strategy.
Custom Source
A Source loads available migrations. Implement the Source interface to load migrations from any media:
type Source interface {
Add(ctx context.Context, migration Migration) error
Load(ctx context.Context) (Repository, error)
}For example, you could load migrations from an S3 bucket, a Git repository, or a configuration service.
The Repository type manages the migration list. Use it in your Load implementation:
func (s *mySource) Load(ctx context.Context) (migrations.Repository, error) {
var repo migrations.Repository
// Load your migrations from wherever they are stored
for _, m := range loadedMigrations {
if err := repo.Add(m); err != nil {
return migrations.Repository{}, err
}
}
return repo, nil
}Custom Target
A Target tracks applied migrations. Implement the Target interface to store state in any backend:
type Target interface {
Current(ctx context.Context) (string, error)
Create(ctx context.Context) error
Destroy(ctx context.Context) error
Done(ctx context.Context) ([]string, error)
Add(ctx context.Context, id string) error
Remove(ctx context.Context, id string) error
FinishMigration(ctx context.Context, id string) error
StartMigration(ctx context.Context, id string) error
Lock(ctx context.Context) (Unlocker, error)
}Key requirements:
Createmust be idempotent.Donemust returnErrDirtyMigrationif any migration is in a dirty state.StartMigrationsets the dirty flag;FinishMigrationclears it.Lockmust prevent concurrent execution. If your backend doesn't support locking, return a no-opUnlocker.
No-op Locker
If your target doesn't need distributed locking:
type noopUnlocker struct{}
func (noopUnlocker) Unlock(ctx context.Context) error { return nil }
func (t *myTarget) Lock(ctx context.Context) (migrations.Unlocker, error) {
return noopUnlocker{}, nil
}Custom Migration
Implement the Migration interface for full control over what a migration does:
type Migration interface {
ID() string
String() string
Description() string
Next() Migration
SetNext(Migration) Migration
Previous() Migration
SetPrevious(Migration) Migration
Do(ctx context.Context) error
CanUndo() bool
Undo(ctx context.Context) error
}Or use the built-in BaseMigration for simple cases:
m := migrations.NewMigration(
"20250401120000",
"create users table",
func(ctx context.Context) error { /* do */ return nil },
func(ctx context.Context) error { /* undo */ return nil }, // nil for forward-only
)Custom Planner
A Planner decides which migrations to run. Implement the ActionPLanner function signature:
type ActionPLanner func(source Source, target Target) Planner
type Planner interface {
Plan(ctx context.Context) (Plan, error)
}Example — a planner that migrates to a specific version:
func MigrateToPlanner(targetID string) migrations.ActionPLanner {
return func(source migrations.Source, target migrations.Target) migrations.Planner {
return &migrateToPlanner{
source: source,
target: target,
targetID: targetID,
}
}
}
type migrateToPlanner struct {
source migrations.Source
target migrations.Target
targetID string
}
func (p *migrateToPlanner) Plan(ctx context.Context) (migrations.Plan, error) {
// Load available and applied migrations
// Build a plan that gets from current state to targetID
// Return actions (Do for forward, Undo for backward)
}Use it with:
migrations.Migrate(ctx, source, target,
migrations.WithPlanner(MigrateToPlanner("20250315143000")),
)