A simple, powerful SQL migration tool for Go with support for both SQL and Go migrations.
Amigo provides a clean API for managing database migrations with built-in CLI support and programmatic access. Write migrations in SQL or Go, control transactions, and get real-time feedback during execution.
- SQL and Go migrations - Write migrations in SQL files or Go code
- Embedded migrations - SQL files are embedded in binary via
embed.FSfor portability - Transaction control - Fine-grained control over transaction behavior
- Multiple database support - PostgreSQL, SQLite, ClickHouse drivers included
- CLI tool - Built-in CLI for managing migrations
- Programmatic API - Use migrations directly in your Go code
- Standard library only - No external dependencies
go get github.com/alexisvisco/amigoCreate the following structure:
yourapp/
├── cmd/
│ └── migrate/
│ └── main.go
├── migrations/
│ └── migrations.go
└── go.mod
Create cmd/migrate/main.go:
package main
import (
"database/sql"
"log"
"os"
"github.com/alexisvisco/amigo"
"yourapp/migrations"
_ "modernc.org/sqlite"
)
func main() {
db, err := sql.Open("sqlite", "app.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
config := amigo.DefaultConfiguration
config.DB = db
config.Driver = amigo.NewSQLiteDriver("")
// Load migrations
migrationList := migrations.Migrations(config)
cli := amigo.NewCLI(amigo.CLIConfig{
Config: config,
Migrations: migrationList,
Directory: "migrations",
DefaultTransactional: true,
DefaultFileFormat: "sql",
})
os.Exit(cli.Run(os.Args[1:]))
}Create migrations/migrations.go:
package migrations
import (
"embed"
"github.com/alexisvisco/amigo"
)
//go:embed *.sql
var sqlFiles embed.FS
func Migrations(cfg amigo.Configuration) []amigo.Migration {
return []amigo.Migration{}
}Note: SQL migrations are embedded using embed.FS, making your migration binary portable with no external SQL files needed.
go run cmd/migrate/main.go generate create_users_tableThis creates migrations/20240101120000_create_users_table.sql:
-- migrate:up tx=true
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
);
-- migrate:down tx=true
DROP TABLE users;And automatically updates migrations/migrations.go:
package migrations
import (
"embed"
"github.com/alexisvisco/amigo"
)
//go:embed *.sql
var sqlFiles embed.FS
func Migrations(cfg amigo.Configuration) []amigo.Migration {
return []amigo.Migration{
amigo.SQLFileToMigration(sqlFiles, "20240101120000_create_users_table.sql", cfg),
}
}The //go:embed *.sql directive embeds all SQL files into the binary, and SQLFileToMigration takes the embedded filesystem as its first argument.
# Apply all pending migrations
go run cmd/migrate/main.go up
# View status
go run cmd/migrate/main.go statusgo build -o bin/migrate cmd/migrate/main.go
# Use it
./bin/migrate up
./bin/migrate status# Generate SQL migration
go run cmd/migrate/main.go generate create_users_table
# Generate Go migration
go run cmd/migrate/main.go generate --format=go add_email_validation# Apply all pending migrations
go run cmd/migrate/main.go up
# Apply next 2 migrations
go run cmd/migrate/main.go up --steps=2
# Skip confirmation
go run cmd/migrate/main.go up --yes# Revert last migration
go run cmd/migrate/main.go down
# Revert last 3 migrations
go run cmd/migrate/main.go down --steps=3
# Revert all migrations
go run cmd/migrate/main.go down --steps=-1
# Skip confirmation
go run cmd/migrate/main.go down --yesgo run cmd/migrate/main.go statusOutput:
Migration Status: 2 applied, 1 pending
Status Date Name Applied At
pending 20240103100000 add_comments
applied 20240102150000 add_posts 2024-01-02 15:30:45
applied 20240101120000 create_users 2024-01-01 12:05:23
go run cmd/migrate/main.go show-configYou can run migrations directly in your Go code without using the CLI:
package main
import (
"context"
"database/sql"
"fmt"
"log"
"github.com/alexisvisco/amigo"
"yourapp/migrations"
_ "modernc.org/sqlite"
)
func main() {
// Open database
db, err := sql.Open("sqlite", "app.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Configure amigo
config := amigo.DefaultConfiguration
config.DB = db
config.Driver = amigo.NewSQLiteDriver("schema_migrations")
// Load migrations
migrationList := migrations.Migrations(config)
// Create runner
runner := amigo.NewRunner(config)
ctx := context.Background()
// Run all pending migrations
err = runner.Up(ctx, migrationList)
if err != nil {
log.Fatalf("Failed to run migrations: %v", err)
}
fmt.Println("Migrations applied successfully!")
}Use iterators for real-time progress:
// Run migrations with progress feedback
for result := range runner.UpIterator(ctx, migrationList) {
if result.Error != nil {
log.Fatalf("Migration failed: %v", result.Error)
}
fmt.Printf("✓ %s (%.2fs)\n", result.Migration.Name(), result.Duration.Seconds())
}// Revert last migration
err = runner.Down(ctx, migrationList, amigo.RunnerDownOptionSteps(1))
// Revert with progress
for result := range runner.DownIterator(ctx, migrationList, amigo.RunnerDownOptionSteps(1)) {
if result.Error != nil {
log.Fatalf("Revert failed: %v", result.Error)
}
fmt.Printf("✓ Reverted %s (%.2fs)\n", result.Migration.Name(), result.Duration.Seconds())
}statuses, err := runner.GetMigrationsStatuses(ctx, migrationList)
if err != nil {
log.Fatal(err)
}
for _, status := range statuses {
if status.Applied {
fmt.Printf("✓ %s (applied at %s)\n",
status.Migration.Name,
status.Migration.AppliedAt.Format("2006-01-02 15:04:05"))
} else {
fmt.Printf("○ %s (pending)\n", status.Migration.Name)
}
}SQL migrations use annotations to separate up and down migrations:
-- migrate:up tx=true
CREATE TABLE posts (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
body TEXT
);
CREATE INDEX idx_posts_title ON posts(title);
-- migrate:down tx=true
DROP TABLE posts;Control transaction behavior per migration:
-- migrate:up tx=false
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
-- migrate:down tx=false
DROP INDEX CONCURRENTLY idx_users_email;Go migrations give you full programmatic control:
package migrations
import (
"context"
"database/sql"
"github.com/alexisvisco/amigo"
)
type Migration20240101120000CreateUsers struct{}
func (m Migration20240101120000CreateUsers) Name() string {
return "create_users"
}
func (m Migration20240101120000CreateUsers) Date() int64 {
return 20240101120000
}
func (m Migration20240101120000CreateUsers) Up(ctx context.Context, db *sql.DB) error {
return amigo.Tx(ctx, db, func(tx *sql.Tx) error {
_, err := tx.Exec(`
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
)
`)
return err
})
}
func (m Migration20240101120000CreateUsers) Down(ctx context.Context, db *sql.DB) error {
return amigo.Tx(ctx, db, func(tx *sql.Tx) error {
_, err := tx.Exec(`DROP TABLE users`)
return err
})
}func (m Migration20240101120000CreateUsers) Up(ctx context.Context, db *sql.DB) error {
_, err := db.ExecContext(ctx, `CREATE INDEX CONCURRENTLY idx_users_email ON users(email)`)
return err
}config := amigo.Configuration{
DB: db,
Driver: driver,
SQLFileUpAnnotation: "-- migrate:up",
SQLFileDownAnnotation: "-- migrate:down",
}cliConfig := amigo.CLIConfig{
Config: config,
Migrations: migrationList,
Output: os.Stdout,
ErrorOut: os.Stderr,
Directory: "db/migrations",
DefaultTransactional: true,
DefaultFileFormat: "sql",
}
cli := amigo.NewCLI(cliConfig)import (
"github.com/alexisvisco/amigo"
_ "github.com/lib/pq"
)
driver := amigo.NewPostgresDriver("schema_migrations")import (
"github.com/alexisvisco/amigo"
_ "modernc.org/sqlite"
)
driver := amigo.NewSQLiteDriver("schema_migrations")import (
"github.com/alexisvisco/amigo"
_ "github.com/ClickHouse/clickhouse-go/v2"
)
driver := amigo.NewClickhouseDriver("schema_migrations")If you have multiple databases (e.g., PostgreSQL for main data and ClickHouse for analytics), create separate migration CLIs:
package main
import (
"database/sql"
"log"
"os"
"github.com/alexisvisco/amigo"
"yourapp/migrations/postgres"
_ "github.com/lib/pq"
)
func main() {
db, err := sql.Open("postgres", "postgres://user:pass@localhost/mydb?sslmode=disable")
if err != nil {
log.Fatal(err)
}
defer db.Close()
config := amigo.DefaultConfiguration
config.DB = db
config.Driver = amigo.NewPostgresDriver("schema_migrations")
migrationList := postgres.Migrations(config)
cli := amigo.NewCLI(amigo.CLIConfig{
Config: config,
Migrations: migrationList,
Directory: "migrations/postgres",
DefaultTransactional: true,
DefaultFileFormat: "sql",
})
os.Exit(cli.Run(os.Args[1:]))
}package main
import (
"database/sql"
"log"
"os"
"github.com/alexisvisco/amigo"
"yourapp/migrations/clickhouse"
_ "github.com/ClickHouse/clickhouse-go/v2"
)
func main() {
db, err := sql.Open("clickhouse", "clickhouse://localhost:9000/default")
if err != nil {
log.Fatal(err)
}
defer db.Close()
config := amigo.DefaultConfiguration
config.DB = db
config.Driver = amigo.NewClickhouseDriver("schema_migrations")
migrationList := clickhouse.Migrations(config)
cli := amigo.NewCLI(amigo.CLIConfig{
Config: config,
Migrations: migrationList,
Directory: "migrations/clickhouse",
DefaultTransactional: true,
DefaultFileFormat: "sql",
})
os.Exit(cli.Run(os.Args[1:]))
}yourapp/
├── cmd/
│ ├── migrate-postgres/
│ │ └── main.go
│ └── migrate-clickhouse/
│ └── main.go
├── migrations/
│ ├── postgres/
│ │ ├── migrations.go
│ │ ├── 20240101120000_create_users.sql
│ │ └── 20240102150000_create_orders.sql
│ └── clickhouse/
│ ├── migrations.go
│ ├── 20240101120000_create_events.sql
│ └── 20240102150000_create_analytics.sql
└── go.mod
# PostgreSQL migrations
go run cmd/migrate-postgres/main.go generate create_users
go run cmd/migrate-postgres/main.go up
go run cmd/migrate-postgres/main.go status
# ClickHouse migrations
go run cmd/migrate-clickhouse/main.go generate create_events
go run cmd/migrate-clickhouse/main.go up
go run cmd/migrate-clickhouse/main.go status# Build both migration tools
go build -o bin/migrate-postgres cmd/migrate-postgres/main.go
go build -o bin/migrate-clickhouse cmd/migrate-clickhouse/main.go
# Use them
./bin/migrate-postgres up
./bin/migrate-clickhouse upMigration files follow the format: {timestamp}_{name}.{ext}
- Timestamp:
YYYYMMDDHHMMSS - Name: Snake case description
- Extension:
.sqlor.go
Example: 20240101120000_create_users_table.sql
Use the Tx helper for transactional Go migrations:
err := amigo.Tx(ctx, db, func(tx *sql.Tx) error {
_, err := tx.Exec("INSERT INTO users (name) VALUES (?)", "Alice")
if err != nil {
return err
}
_, err = tx.Exec("INSERT INTO posts (title) VALUES (?)", "First Post")
return err
})MIT
Contributions welcome! Please open an issue or PR.