Migrations Overview
Modular, Go-code migrations with dependency-aware ordering.
Grove migrations are Go code, not SQL files. Any Go module can register migrations into named groups with dependency-aware ordering. Forge extensions ship their own migrations that compose automatically.
Key Concepts
- Migrations are Go functions — not SQL files, enabling logic, conditionals, and data transformations
- Groups — Migrations belong to named groups (e.g.,
"core","forge.billing") - Dependencies — Groups declare dependencies on other groups for ordering
- Topological Sort — The migrator resolves execution order automatically
- Locking — Advisory locks prevent concurrent migration runs
Defining Migrations
import "github.com/xraph/grove/migrate"
var Migrations = migrate.NewGroup("core")
func init() {
Migrations.MustRegister(&migrate.Migration{
Name: "create_users",
Version: "20240101000000",
Up: func(ctx context.Context, exec migrate.Executor) error {
_, err := exec.NewCreateTable((*User)(nil)).
IfNotExists().
Exec(ctx)
return err
},
Down: func(ctx context.Context, exec migrate.Executor) error {
_, err := exec.NewDropTable((*User)(nil)).
IfExists().
Exec(ctx)
return err
},
})
}Running Migrations
import "github.com/xraph/grove/drivers/pgdriver/pgmigrate"
// Create a driver-specific executor
pgdb := pgdriver.Unwrap(db)
executor := pgmigrate.NewExecutor(pgdb)
// Create the orchestrator with the executor
orchestrator := migrate.NewOrchestrator(executor,
core.Migrations,
billing.Migrations,
)
// Migrate up
result, err := orchestrator.Migrate(ctx)
fmt.Printf("Applied %d migrations\n", len(result.Applied))
// Rollback last migration
result, err = orchestrator.Rollback(ctx)
// Check status
statuses, err := orchestrator.Status(ctx)Migration Table
Grove tracks migration state in grove_migrations:
CREATE TABLE grove_migrations (
id SERIAL PRIMARY KEY,
group_name VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
version VARCHAR(255) NOT NULL,
batch INTEGER NOT NULL,
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(group_name, version)
);Executor Registry
The executor registry provides driver-agnostic executor creation through auto-registration. Each driver's migrate package registers its factory via init(), so a blank import is all you need.
Auto-Registration
Import the driver's migrate package to register its executor factory:
| Import | Registered Name |
|---|---|
_ "github.com/xraph/grove/drivers/pgdriver/pgmigrate" | pg |
_ "github.com/xraph/grove/drivers/mysqldriver/mysqlmigrate" | mysql |
_ "github.com/xraph/grove/drivers/sqlitedriver/sqlitemigrate" | sqlite |
_ "github.com/xraph/grove/drivers/tursodriver/tursomigrate" | turso |
_ "github.com/xraph/grove/drivers/clickhousedriver/clickhousemigrate" | clickhouse |
_ "github.com/xraph/grove/drivers/mongodriver/mongomigrate" | mongo |
Using the Registry
Once registered, create executors from any driver dynamically:
import "github.com/xraph/grove/migrate"
executor, err := migrate.NewExecutorFor(db.Driver())
if err != nil {
// No factory registered for this driver
log.Fatal(err)
}
orch := migrate.NewOrchestrator(executor, core.Migrations)Manual Registration
Register a custom executor factory for drivers not included in Grove:
migrate.RegisterExecutor("custom", func(drv any) migrate.Executor {
return myCustomExecutor(drv)
})Listing Registered Executors
names := migrate.Executors()
fmt.Println("Registered executors:", names) // e.g. [pg mysql sqlite]When using the Grove Forge extension, executor registration is handled automatically — you only need the blank import for your driver's migrate package.