A flexible, easy-to-use database migration library for Go applications. This library allows you to programmatically manage database schema migrations with support for embedded migrations, filesystem migrations, and custom migration sources.
- 📦 Embedded Migrations: Embed SQL files directly into your binary using Go's
embedpackage - 📁 Filesystem Migrations: Load migrations from a directory at runtime
- 🔌 Extensible: Implement custom migration sources for advanced use cases
- 📊 Automatic Migration Tracking: Tracking table is automatically created - no manual setup required
- 🔄 Transaction Support: Each migration runs in a transaction for safety
- 🎯 Simple API: Clean, intuitive API with sensible defaults
- ✅ Status Checking: Query current version and pending migrations
- 🔧 Flexible Options: Dry-run mode, verbose logging, custom context, custom table names
- 🏷️ Custom Table Names: Use custom table names for tracking migrations to avoid conflicts
go get github.com/gurumfj/migration-goimport (
"database/sql"
"log"
migration "github.com/gurumfj/migration-go"
_ "github.com/mattn/go-sqlite3"
)
func main() {
db, err := sql.Open("sqlite3", "./app.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Run all pending migrations
result, err := migration.Run(db)
if err != nil {
log.Fatal(err)
}
log.Printf("Applied %d migrations, current version: %s",
result.Applied, result.CurrentVersion)
}The real power of this library comes from using it in your own projects with custom migrations.
Create your migration files and embed them in your application:
Project Structure:
your-project/
├── main.go
├── migrations/
│ ├── 000_initial_schema.sql
│ ├── 001_add_users_table.sql
│ └── 002_add_posts_table.sql
└── go.mod
main.go:
package main
import (
"database/sql"
"embed"
"log"
migration "github.com/gurumfj/migration-go"
_ "github.com/mattn/go-sqlite3"
)
//go:embed migrations/*.sql
var migrations embed.FS
func main() {
db, err := sql.Open("sqlite3", "./myapp.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Create migrator with your embedded migrations
migrator := migration.NewMigratorFromFS(migrations, "migrations")
// Run migrations
result, err := migrator.Run(db, migration.WithVerbose())
if err != nil {
log.Fatalf("Migration failed: %v", err)
}
log.Printf("✅ Applied %d migrations", result.Applied)
log.Printf("📊 Current version: %s", result.CurrentVersion)
}Load migrations from a directory at runtime (useful for development):
package main
import (
"database/sql"
"log"
migration "github.com/gurumfj/migration-go"
_ "github.com/mattn/go-sqlite3"
)
func main() {
db, _ := sql.Open("sqlite3", "./myapp.db")
defer db.Close()
// Create migrator that reads from filesystem
migrator := migration.NewMigratorFromDir("./db/migrations")
// Run migrations
result, err := migrator.Run(db)
if err != nil {
log.Fatal(err)
}
log.Printf("Applied %d migrations", result.Applied)
}For advanced use cases, implement the MigrationSource interface:
package main
import (
migration "github.com/gurumfj/migration-go"
)
// CustomSource loads migrations from your custom location
// (e.g., database, HTTP API, encrypted files, etc.)
type CustomSource struct {
apiURL string
}
func (s *CustomSource) ReadMigrations() ([]migration.Migration, error) {
// Your custom logic here
// Fetch migrations from API, decrypt files, etc.
return []migration.Migration{
{
ID: "000",
Description: "initial schema",
Content: "CREATE TABLE ...",
},
}, nil
}
func main() {
db, _ := sql.Open("sqlite3", "./myapp.db")
defer db.Close()
// Use custom source
source := &CustomSource{apiURL: "https://api.example.com/migrations"}
migrator := migration.NewMigrator(source)
result, err := migrator.Run(db)
// ...
}Migration files must follow this naming convention:
{ID}_{description}.sql
- ID: Zero-padded number (e.g.,
000,001,002) - Description: Underscore-separated description
Examples:
000_initial_schema.sql001_add_users_table.sql002_add_email_index.sql
Sample Migration File:
-- 001_add_users_table.sql
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_email ON users(email);These use the library's embedded default migrations:
// Run all pending migrations
result, err := migration.Run(db, options...)
// Get current migration status
status, err := migration.Status(db)
// Get current version
version, err := migration.GetCurrentVersion(db)For custom migrations, create a Migrator first:
// Create from embed.FS
migrator := migration.NewMigratorFromFS(fsys, "migrations")
// Create from directory
migrator := migration.NewMigratorFromDir("./db/migrations")
// Create from custom source
migrator := migration.NewMigrator(customSource)
// Run migrations
result, err := migrator.Run(db, options...)
// Check status
status, err := migrator.Status(db)// Dry-run mode (shows what would be applied without actually applying)
migration.WithDryRun()
// Verbose logging
migration.WithVerbose()
// Custom context
migration.WithContext(ctx)
// Custom table name for tracking migrations (default: "schema_migrations")
migration.WithTableName("custom_migrations")Example:
result, err := migrator.Run(db,
migration.WithVerbose(),
migration.WithContext(ctx),
)
// Using custom table name
result, err := migrator.Run(db,
migration.WithTableName("my_migrations"),
migration.WithVerbose(),
)type MigrationResult struct {
Applied int // Number of migrations applied
Skipped int // Number already applied
CurrentVersion string // Current database version
AppliedAt time.Time // When completed
}type MigrationStatus struct {
CurrentVersion string // Current version
PendingMigrations []Migration // Pending migrations
AppliedMigrations []Migration // Applied migrations
IsUpToDate bool // All migrations applied?
}type Migration struct {
ID string // Migration ID (e.g., "001")
Description string // Human-readable description
Content string // SQL content
AppliedAt *time.Time // When applied (nil if pending)
}func main() {
db := setupDatabase()
defer db.Close()
// Run migrations automatically on startup
log.Println("Running database migrations...")
migrator := migration.NewMigratorFromFS(migrations, "migrations")
result, err := migrator.Run(db)
if err != nil {
log.Fatalf("Migration failed: %v", err)
}
if result.Applied > 0 {
log.Printf("✅ Applied %d new migrations", result.Applied)
} else {
log.Println("✅ Database is up to date")
}
// Start application...
}func checkMigrationStatus(db *sql.DB) {
migrator := migration.NewMigratorFromFS(migrations, "migrations")
status, err := migrator.Status(db)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Current Version: %s\n", status.CurrentVersion)
fmt.Printf("Up to Date: %v\n", status.IsUpToDate)
if len(status.PendingMigrations) > 0 {
fmt.Println("\nPending Migrations:")
for _, m := range status.PendingMigrations {
fmt.Printf(" - %s: %s\n", m.ID, m.Description)
}
}
}func safeMigration(db *sql.DB) {
migrator := migration.NewMigratorFromFS(migrations, "migrations")
// First, do a dry run to see what would be applied
result, err := migrator.Run(db,
migration.WithDryRun(),
migration.WithVerbose(),
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Would apply %d migrations\n", result.Applied)
// Prompt user for confirmation...
if userConfirms() {
// Actually run the migrations
result, err = migrator.Run(db, migration.WithVerbose())
if err != nil {
log.Fatal(err)
}
fmt.Printf("Applied %d migrations successfully\n", result.Applied)
}
}func migrateMultipleDatabases() {
// Different projects can use different migration sources
// Project A
dbA, _ := sql.Open("sqlite3", "./projectA.db")
migratorA := migration.NewMigratorFromFS(migrationsA, "migrations")
migratorA.Run(dbA)
// Project B
dbB, _ := sql.Open("postgres", "postgres://...")
migratorB := migration.NewMigratorFromDir("./project-b/migrations")
migratorB.Run(dbB)
}func migrateWithCustomTable(db *sql.DB) {
migrator := migration.NewMigratorFromFS(migrations, "migrations")
// Use a custom table name to avoid conflicts
customTable := "app_migrations"
// Run migrations with custom table
result, err := migrator.Run(db,
migration.WithTableName(customTable),
migration.WithVerbose(),
)
if err != nil {
log.Fatalf("Migration failed: %v", err)
}
log.Printf("Applied %d migrations to %s table", result.Applied, customTable)
// Check status with same custom table
status, err := migrator.Status(db, migration.WithTableName(customTable))
if err != nil {
log.Fatal(err)
}
log.Printf("Current version: %s", status.CurrentVersion)
}The library uses a simple, linear migration strategy:
- Sequential IDs: Migrations are numbered sequentially (000, 001, 002, ...)
- Automatic Tracking Table: The
schema_migrationstable is automatically created by the library - you don't need to create it manually - Incremental Changes: Each subsequent migration builds on the previous state
- Transaction Safety: Each migration runs in its own transaction
- Idempotent: Safe to run multiple times (already-applied migrations are skipped)
- Custom Table Names: You can use a custom table name for tracking migrations via
WithTableName()option
This library works with any SQL database supported by Go's database/sql package:
- ✅ SQLite
- ✅ PostgreSQL
- ✅ MySQL/MariaDB
- ✅ SQL Server
- ✅ Oracle
- ✅ CockroachDB
Just import the appropriate driver:
_ "github.com/mattn/go-sqlite3" // SQLite
_ "github.com/lib/pq" // PostgreSQL
_ "github.com/go-sql-driver/mysql" // MySQL
_ "github.com/denisenkom/go-mssqldb" // SQL Server- Always Use Transactions: The library automatically wraps each migration in a transaction
- Keep Migrations Small: One logical change per migration
- Never Modify Applied Migrations: Once a migration is applied, create a new one for changes
- Use Descriptive Names:
001_add_user_table.sqlis better than001_update.sql - Test Migrations: Test on a copy of production data before applying to production
- Version Control: Commit all migration files to version control
- Sequential IDs: Always use the next sequential number for new migrations
If a migration fails:
- The transaction is rolled back automatically
- No migration record is created
- The error is returned to the caller
- Fix the migration SQL and try again
The library automatically creates a schema_migrations table to track applied migrations when you run migrator.Run():
CREATE TABLE IF NOT EXISTS schema_migrations (
id TEXT PRIMARY KEY,
description TEXT NOT NULL,
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
);You can query this table directly to see migration history:
SELECT * FROM schema_migrations ORDER BY id;Using a Custom Table Name:
If you need to use a different table name (e.g., to avoid conflicts with existing tables), use the WithTableName() option:
// Use a custom table name for tracking migrations
migrator := migration.NewMigratorFromFS(migrations, "migrations")
result, err := migrator.Run(db, migration.WithTableName("my_app_migrations"))
// Query the custom table
db.Query("SELECT * FROM my_app_migrations ORDER BY id")
// Get version from custom table
version, err := migration.GetCurrentVersionWithTable(db, "my_app_migrations")See the examples directory for complete working examples:
-
Basic Usage - Comprehensive examples demonstrating all library features
- Basic migration usage
- Status checking
- Verbose output
- Version tracking
- Filesystem migrations
-
Embedded Migrations - Production-ready application with embedded migrations
- Complete startup flow
- Migration status checking
- Error handling
- Database connection management
Each example includes:
- Complete, runnable code
- Detailed README with explanations
- Sample migration files
- Expected output
Contributions are welcome! Please ensure:
- Tests pass
- Code is formatted with
go fmt - Documentation is updated
- Examples are provided for new features
MIT License