Grove

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:

ImportRegistered 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.

On this page