Migration manager for the Gas ecosystem. Tracks and applies database migrations across all Gas services with dirty-state detection and rollback support.
go get github.com/gasmod/gas-migrate
package main
import (
"github.com/gasmod/gas"
database "github.com/gasmod/gas-database"
migrate "github.com/gasmod/gas-migrate"
)
func main() {
app := gas.NewApp(
gas.WithSingletonService[*database.Service](database.New()),
gas.WithSingletonService[*migrate.Service](migrate.New()),
gas.WithSingletonService[*auth.Service](auth.New),
)
app.Run()
}Services register their migrations during Init(). There are three ways to register.
func (s *Service) Init() error {
s.migrationMgr.Register(s.Name(), gas.Migration{
Version: "20250216001",
Description: "create users table",
Up: "CREATE TABLE users (id SERIAL PRIMARY KEY, email TEXT NOT NULL);",
Down: "DROP TABLE users;",
})
return nil
}func (s *Service) Init() error {
s.migrationMgr.RegisterSlice(s.Name(), []gas.Migration{
{
Version: "20250216001",
Description: "create users table",
Up: "CREATE TABLE users (id SERIAL PRIMARY KEY, email TEXT NOT NULL);",
Down: "DROP TABLE users;",
},
{
Version: "20250216002",
Description: "create sessions table",
Up: "CREATE TABLE sessions (id TEXT PRIMARY KEY, user_id INT REFERENCES users(id));",
Down: "DROP TABLE sessions;",
},
})
return nil
}import "embed"
//go:embed migrations/*.sql
var migrationsFS embed.FS
func (s *Service) Init() error {
return s.migrationMgr.RegisterFS(s.Name(), migrationsFS)
}Files must follow this naming convention:
migrations/
20250216001_create_users.up.sql
20250216001_create_users.down.sql
20250216002_create_sessions.up.sql
20250216002_create_sessions.down.sql
The version is the first underscore-delimited segment (e.g. 20250216001), and the description is parsed from the
remaining segments (underscores become spaces).
// Apply all pending migrations in global version order.
err := migrationMgr.RunPending()
// Roll back the last 2 applied migrations.
err := migrationMgr.Down(2)- Migrations are tracked in a
__gas_migrationstable created automatically onInit(). Init()selects the correct sqlc-generated query adapter based on the database driver (PostgreSQL, MySQL, or SQLite). Unsupported drivers causeInit()to return an error.RunPending()sorts all registered migrations globally by version across all services and applies any that haven't been applied yet.- Each migration runs in its own transaction. If a migration fails, it is marked dirty and all further execution is blocked until the dirty state is manually resolved.
Down(n)reverses the lastnapplied migrations in reverse version order.- Version collision detection: If two services register migrations with the same version,
RunPending()andDown()return an error identifying the conflicting version and both service names.
The service implements gas.ReadyReporter. CheckReady(ctx) returns nil only when the service is initialized, not
closed, has no dirty migrations, and has no registered migrations pending application. This maps to a Kubernetes
readinessProbe — traffic is gated until migrations have applied successfully. gas.HealthReporter is intentionally
not implemented, since gas-migrate owns no live runtime state distinct from the underlying database connection
(which gas-database reports on).
If a migration fails, it is recorded as dirty in the tracking table. Subsequent calls to RunPending() will return an
error listing the dirty versions. To resolve:
- Fix the underlying issue (bad SQL, missing dependency, etc.).
- Manually remove or update the dirty row in
__gas_migrations. - Run
RunPending()again.