Production-ready database migration tool for Go with shadow database testing and transaction safety.
- 🔒 Transaction-Safe: All migrations run in transactions with automatic rollback on failure
- 🧪 Shadow Database Testing: Test migrations on a shadow database before applying to production
- ✅ Validation: Ensures migration consistency between database and filesystem
- 📊 PostgreSQL Support: Built specifically for PostgreSQL with proper connection handling
- 🎯 Simple API: Clean, easy-to-use interface
- 📝 Detailed Logging: Clear output showing migration progress
- 🔄 Idempotent: Safe to run multiple times
- ⚡ Context Support: Proper context handling with timeout support
- 🧩 Modular Architecture: Clean internal package structure
# Latest stable version (recommended)
go get github.com/hasirciogluhq/migrator@latest
# Specific version
go get github.com/hasirciogluhq/migrator@v1.0.0Version Policy:
- 🤖 Fully automated releases - Every push to main triggers CI/CD
- ✅ Test-protected - Only releases if all tests pass
- 📦 Semantic versioning - Auto-calculated from commit messages
- 🔄 Always safe - Failed tests = no release, use previous version
- 📖 See RELEASE.md for details
This package uses fully automated CI/CD. Just push to main:
# Bug fix (v1.0.0 → v1.0.1)
git commit -m "[*] Fixed migration bug"
git push origin main
# New feature (v1.0.0 → v1.1.0)
git commit -m "[+] Added rollback support"
git push origin main
# Breaking change (v1.0.0 → v2.0.0)
git commit -m "[MAJOR] Changed API signature"
git push origin main✅ Tests pass → Automatic release
❌ Tests fail → No release, safe!
See RELEASE.md for commit message format.
mkdir migrationsCreate SQL files in your migrations directory. Files are executed in alphabetical order.
migrations/001_create_users.sql:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_email ON users(email);migrations/002_create_posts.sql:
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
title VARCHAR(255) NOT NULL,
content TEXT,
published BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_posts_user_id ON posts(user_id);package main
import (
"context"
"database/sql"
"log"
_ "github.com/lib/pq"
"github.com/hasirciogluhq/migrator"
)
func main() {
// Connect to your database
db, err := sql.Open("postgres",
"postgres://user:password@localhost:5432/mydb?sslmode=disable")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Create migrator
m := migrator.New(db)
// Run migrations
if err := m.Migrate(context.Background()); err != nil {
log.Fatal(err)
}
log.Println("Migrations completed successfully!")
}DATABASE_URL: PostgreSQL connection string (optional, fallback for shadow database operations)MIGRATIONS_PATH: Path to migrations directory (default:./migrations)
m := migrator.NewWithOptions(db, migrator.Options{
MigrationsPath: "./db/migrations",
DatabaseURL: "postgres://user:pass@localhost:5432/mydb", // Optional, for shadow DB testing
SkipShadowDB: false, // Set to true to skip shadow database testing
})Shadow Database Testing: Shadow database testing requires a database URL to create temporary test databases. You can provide it in two ways:
- Recommended (Production): Pass it in
Options.DatabaseURL - Fallback: Set
DATABASE_URLenvironment variable
If neither is provided, shadow database testing will be skipped with a warning.
The migrator follows a robust, multi-step process:
- Ensure Tracking Table: Creates
_go_migrationstable if it doesn't exist - Validate Existing Migrations: Verifies all applied migrations still exist in filesystem
- Load Migration Files: Reads all
.sqlfiles from migrations directory - Shadow Database Testing:
- Creates a temporary shadow database
- Applies existing migrations to shadow database
- Tests new migrations on shadow database
- Drops shadow database after testing
- Apply to Production: Applies pending migrations to production database
- Cleanup: Ensures shadow database is removed
Each migration runs in its own transaction:
- ✅ If successful: Changes are committed and migration is recorded
- ❌ If failed: Changes are rolled back and migration is not recorded
Before applying to production, new migrations are tested on a shadow database:
your_database → Production database (untouched during testing)
your_database_gi_mig_shadow_db → Temporary shadow database (created, tested, dropped)
This ensures:
- Syntax errors are caught before production
- Migrations are compatible with existing schema
- No surprises in production deployment
Creates a new migrator with default options.
m := migrator.New(db)Creates a new migrator with custom options.
m := migrator.NewWithOptions(db, migrator.Options{
MigrationsPath: "./custom/path",
SkipShadowDB: false,
})Runs the complete migration process.
if err := m.Migrate(context.Background()); err != nil {
log.Fatal(err)
}Returns a list of all applied migration names.
applied, err := m.GetAppliedMigrations(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Println("Applied migrations:", applied)Returns a list of migrations that haven't been applied yet.
pending, err := m.GetPendingMigrations(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Printf("Pending migrations: %d\n", len(pending))See examples/basic/main.go for a complete example.
os.Setenv("MIGRATIONS_PATH", "./db/migrations")
m := migrator.New(db)
m.Migrate(context.Background())ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
if err := m.Migrate(ctx); err != nil {
log.Fatal(err)
}m := migrator.New(db)
// Get applied migrations
applied, err := m.GetAppliedMigrations(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Printf("Applied: %v\n", applied)
// Get pending migrations
pending, err := m.GetPendingMigrations(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Printf("Pending: %d migrations\n", len(pending))Use a consistent naming convention:
001_create_users.sql
002_create_posts.sql
003_add_user_avatar.sql
- Be Explicit: Always specify column types, constraints, and defaults
- Use Transactions: Each file runs in a transaction, so keep related changes together
- Add Indexes: Create indexes in the same migration as the table
- Handle Data: You can include INSERT/UPDATE statements for seed data
- Test Locally: Always test migrations locally first
- Backup: Take database backups before running migrations in production
- Monitor: Watch logs during migration execution
- Rollback Plan: Have a rollback strategy for each migration
✅ Do:
- Keep migrations small and focused
- Test migrations thoroughly
- Use descriptive file names
- Version control your migrations
- Run migrations during deployment automation
❌ Don't:
- Modify already-applied migrations
- Delete migration files that have been applied
- Include DROP statements without careful consideration
- Skip shadow database testing in production
The package includes comprehensive tests with 11 test scenarios covering:
- ✅ Basic migration flow
- ✅ Idempotent migrations
- ✅ Transaction rollback on failure
- ✅ Incremental migrations
- ✅ Missing file detection
- ✅ Complex multi-statement migrations
- ✅ Concurrent migrations
- ✅ Context cancellation
The easiest way to run tests:
# Run tests with Docker (automatically starts/stops PostgreSQL)
make test-docker
# Run tests with existing PostgreSQL
make test
# Run tests with coverage report
make test-coverage
# See all available commands
make helpOption 1: Using Docker (Recommended)
# Start PostgreSQL
docker run -d --name migrator-test \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_USER=postgres \
-e POSTGRES_DB=postgres \
-p 5432:5432 \
postgres
# Wait for PostgreSQL to be ready
sleep 3
# Set connection string and run tests
export DATABASE_URL="postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable"
go test -v ./...
# Cleanup
docker stop migrator-test && docker rm migrator-testOption 2: Using Existing PostgreSQL
# Set your PostgreSQL connection string
export DATABASE_URL="postgres://YOUR_USER:YOUR_PASSWORD@localhost:5432/postgres?sslmode=disable"
# Run tests
go test -v ./...Run with Coverage:
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
go tool cover -html=coverage.txtTests automatically run on GitHub Actions on every push/PR. The workflow:
- Spins up PostgreSQL 15 service
- Runs
go vetfor code quality - Executes all tests with race detector
- Uploads coverage to Codecov
See .github/workflows/test.yml for details.
migrator/
├── migrator.go # Public API
├── internal/
│ ├── tracker/ # Migration tracking & database operations
│ │ └── tracker.go
│ ├── validator/ # Migration validation & file handling
│ │ └── validator.go
│ └── shadowdb/ # Shadow database management
│ └── shadowdb.go
├── migrator_test.go # Comprehensive test suite
└── examples/ # Usage examples
If you see a warning about DATABASE_URL not being provided, you have two options:
Option 1 (Recommended): Pass the database URL explicitly in options:
m := migrator.NewWithOptions(db, migrator.Options{
DatabaseURL: "postgres://user:password@localhost:5432/mydb",
})Option 2: Set the DATABASE_URL environment variable:
export DATABASE_URL="postgres://user:password@localhost:5432/mydb"Note: Shadow database testing will be skipped if no database URL is provided.
This means migrations that were previously applied have been deleted from your migrations directory. This is a safety check to prevent inconsistencies. You need to restore the missing migration files.
The shadow database cleanup failed. You can manually drop it:
DROP DATABASE IF EXISTS your_database_gi_mig_shadow_db;Each migration has a 5-minute timeout by default. If your migration needs more time, consider:
- Breaking it into smaller migrations
- Optimizing the SQL queries
- Running heavy operations outside migration system
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
- Inspired by database migration tools like Flyway and golang-migrate
- Built with production reliability and developer experience in mind
- 📫 Issues: GitHub Issues
- 💬 Discussions: GitHub Discussions
Made with ❤️ for the Go community