Skip to content

Latest commit

 

History

History
485 lines (332 loc) · 12.7 KB

File metadata and controls

485 lines (332 loc) · 12.7 KB

migrate — Compiled Migration Binary

The migrate binary is the runtime component of the Go migration framework. It is compiled from the migrations/ directory in your project and is the primary way to apply, rollback, and inspect database migrations.

This is the primary workflow. Run makemigrations init to generate the migrations/ directory, then use makemigrations migrate or compile the binary manually as shown below.

Overview

Each project has its own compiled migration binary that embeds all registered migrations. The binary is built from Go source files in the migrations/ directory and knows exactly which migrations exist, their dependencies, and their SQL.

your-project/
└── migrations/
    ├── main.go             ← binary entry point (generated by init)
    ├── go.mod              ← module file (generated by init)
    ├── 0001_initial.go     ← migration files (generated by makemigrations)
    ├── 0002_add_phone.go
    └── migrate             ← compiled binary (you build this)

Running via makemigrations migrate (Recommended)

makemigrations migrate automatically builds the migrations binary with the correct Go workspace and toolchain settings, then runs it. All arguments are forwarded unchanged.

makemigrations migrate up
makemigrations migrate down --steps 2
makemigrations migrate status
makemigrations migrate showsql
makemigrations migrate fake 0001_initial
makemigrations migrate dag

Add --verbose to see the build step:

makemigrations migrate --verbose up

This is the recommended approach during development because it handles Go workspace conflicts and toolchain version selection automatically. See the Manual Build Guide for how to build the binary yourself.

Building the Binary Manually

cd migrations && go mod tidy && go build -o migrate .

Run this after:

  • makemigrations init (first time setup)
  • makemigrations makemigrations (after generating new migrations)

If your project uses a go.work file or a non-system Go toolchain, see the Manual Build Guide for the correct environment flags.

Commands

dag

Show the migration dependency graph.

./migrations/migrate dag [--format ascii|json]

Flags:

Flag Default Description
--format ascii Output format: ascii for a text tree, json for machine-readable output

ASCII output example:

0001_initial
└── 0002_add_phone
    └── 0003_add_index

JSON output example:

{
  "nodes": ["0001_initial", "0002_add_phone", "0003_add_index"],
  "edges": {
    "0002_add_phone": ["0001_initial"],
    "0003_add_index": ["0002_add_phone"]
  },
  "leaves": ["0003_add_index"],
  "has_branches": false,
  "schema_state": { ... }
}

The --format json output is used internally by makemigrations makemigrations to reconstruct schema state before diffing.


up

Apply all pending migrations in topological order.

./migrations/migrate up [--to <migration-name>] [--warn-on-missing-drop]

Flags:

Flag Default Description
--to (none) Stop after applying the named migration
--warn-on-missing-drop false Warn and continue when a DROP TABLE, DROP COLUMN, or DROP INDEX fails because the object does not exist

Examples:

# Apply all pending migrations
./migrations/migrate up

# Apply only up to a specific migration
./migrations/migrate up --to 0003_add_index

# Continue past drop operations that target objects already absent from the database
./migrations/migrate up --warn-on-missing-drop

Output:

Applying 0001_initial... done
Applying 0002_add_phone... done
Applying 0003_add_index... done

If a migration fails, it prints FAILED and returns a non-zero exit code. Migrations already applied are skipped automatically.


down

Roll back applied migrations in reverse topological order.

./migrations/migrate down [--steps <n>] [--to <migration-name>] [--warn-on-missing-drop]

Flags:

Flag Default Description
--steps 1 Number of migrations to roll back
--to (none) Roll back until (but not including) this migration name
--warn-on-missing-drop false Warn and continue when a DROP TABLE, DROP COLUMN, or DROP INDEX fails because the object does not exist

Examples:

# Roll back the most recent migration (default)
./migrations/migrate down

# Roll back the last 3 migrations
./migrations/migrate down --steps 3

# Roll back until 0001_initial (rolls back everything after it)
./migrations/migrate down --to 0001_initial

# Continue past drop operations that target objects already absent from the database
./migrations/migrate down --warn-on-missing-drop

Output:

Rolling back 0003_add_index... done
Rolling back 0002_add_phone... done

status

Show which migrations have been applied and which are pending.

./migrations/migrate status

Output:

Migration                                          Status
------------------------------------------------------------
0001_initial                                       Applied
0002_add_phone                                     Applied
0003_add_index                                     Pending

Run this before and after up/down to verify state.


showsql

Print the SQL for all pending migrations without executing it.

./migrations/migrate showsql

Output:

-- 0003_add_index
CREATE INDEX idx_users_phone ON users (phone);

Use this to review what SQL will run before committing to up. Useful for auditing, security reviews, and debugging provider-specific SQL generation.

Already-applied migrations are skipped. The SQL is generated using the current provider (database type configured in main.go).


fake

Mark a migration as applied in the history table without executing its SQL.

./migrations/migrate fake <migration-name>

Arguments:

Argument Required Description
migration-name Yes The exact name of the migration to fake

Examples:

# Fake a single migration (e.g., for an already-applied initial schema)
./migrations/migrate fake 0001_initial

# Fake all migrations at once using a script loop
for m in 0001_initial 0002_add_phone; do
  ./migrations/migrate fake "$m"
done

Output:

Marked "0001_initial" as applied (faked).

When to use fake:

This is primarily used when setting up Go migrations on an existing database. The makemigrations init command prints the exact fake commands needed after it generates 0001_initial.go from a schema snapshot.

Your database already has these tables applied. Mark this migration as applied without re-running SQL:

  cd migrations && go mod tidy && go build -o migrate .
  ./migrate fake 0001_initial

Database Configuration

The compiled binary connects to the database using the configuration embedded in migrations/main.go. The file generated by makemigrations init looks like this:

package main

import (
    "fmt"
    "os"

    m "github.com/ocomsoft/makemigrations/migrate"
)

func main() {
    app := m.NewApp(m.Config{
        DatabaseType: m.EnvOr("DB_TYPE", "postgresql"),
        DatabaseURL:  m.EnvOr("DATABASE_URL", ""),
    })
    if err := app.Run(os.Args[1:]); err != nil {
        fmt.Fprintf(os.Stderr, "Error: %v\n", err)
        os.Exit(1)
    }
}

Environment Variables

Variable Default Description
DATABASE_URL "" Full DSN (e.g. postgresql://user:pass@host/db)
DB_TYPE postgresql Database type: postgresql, mysql, sqlserver, sqlite

Individual connection fields

migrate.Config also has DBHost, DBPort, DBUser, DBPassword, DBName, and DBSSLMode fields. They are not wired up in the generated main.go — edit the file to add them when you need per-field connection configuration:

app := m.NewApp(m.Config{
    DatabaseType: m.EnvOr("DB_TYPE", "postgresql"),
    DatabaseURL:  m.EnvOr("DATABASE_URL", ""),
    DBHost:       m.EnvOr("DB_HOST", "localhost"),
    DBPort:       m.EnvOr("DB_PORT", "5432"),
    DBUser:       m.EnvOr("DB_USER", "postgres"),
    DBPassword:   os.Getenv("DB_PASSWORD"),
    DBName:       m.EnvOr("DB_NAME", "mydb"),
    DBSSLMode:    m.EnvOr("DB_SSLMODE", "disable"),
})

DATABASE_URL takes priority over the individual fields when both are set. See the Manual Build Guide for a full example.

The variable names and defaults are entirely determined by your migrations/main.go. Customise them freely to match your project's conventions.


Migration History Table

The binary stores migration history in a table called makemigrations_history:

CREATE TABLE IF NOT EXISTS makemigrations_history (
    id         INTEGER PRIMARY KEY,
    name       VARCHAR(255) NOT NULL UNIQUE,
    applied_at TIMESTAMP    NOT NULL
);

This table is created automatically on first up or status run. It is created using portable SQL compatible with PostgreSQL, MySQL, SQLite, and SQL Server.


Complete Workflow

First-time setup on a new database

# 1. Generate initial migration (if you have an existing schema snapshot)
makemigrations init

# 2. Apply all migrations
makemigrations migrate up

# 3. Verify
makemigrations migrate status

First-time setup on an existing database (fake initial)

# 1. Generate initial migration from existing schema
makemigrations init
# This prints: makemigrations migrate fake 0001_initial

# 2. Mark the initial migration as already applied
makemigrations migrate fake 0001_initial

# 3. Verify
makemigrations migrate status

Day-to-day development cycle

# 1. Edit your YAML schema files

# 2. Generate the migration
makemigrations makemigrations --name "add user preferences"
# Creates: migrations/0004_add_user_preferences.go

# 3. Preview the SQL before applying
makemigrations migrate showsql

# 4. Apply
makemigrations migrate up

# 5. Verify
makemigrations migrate status

CI/CD pipeline

# Check whether any unapplied migrations exist (fails if pending)
makemigrations makemigrations --check

# Apply migrations as part of deployment
makemigrations migrate up

Rollback in production

# Review what will be rolled back
makemigrations migrate status

# Roll back the last migration
makemigrations migrate down

# Or roll back multiple steps
makemigrations migrate down --steps 3

Branch / Merge Scenarios

When two developers create migrations concurrently, the DAG develops branches:

0001_initial
├── 0002_branch_a   (developer A)
└── 0003_branch_b   (developer B)

The dag command shows this:

./migrate dag
0001_initial
├── 0002_branch_a
└── 0003_branch_b

The makemigrations makemigrations command will warn about branches. Resolve with a merge migration:

makemigrations makemigrations --merge
# Creates: migrations/0004_merge_0002_branch_a_and_0003_branch_b.go

After a merge, the DAG converges to a single leaf again.


Troubleshooting

"building graph: cycle detected"

Your migration dependencies form a cycle. Check Dependencies fields in your .go migration files.

"opening database: ..."

Database connection failed. Check your environment variables and that the database is reachable:

echo $DATABASE_URL
echo $DB_HOST $DB_USER $DB_NAME

Migration fails mid-run

The binary stops at the first failure and prints FAILED. Migrations already completed in that run are recorded as applied. Fix the failing migration and re-run up.

"DROP TABLE / DROP COLUMN / DROP INDEX" fails because the object doesn't exist

If someone manually removed a database object that a migration is trying to drop, up or down will fail. Use --warn-on-missing-drop to print a warning and continue instead of stopping:

./migrations/migrate up --warn-on-missing-drop
./migrations/migrate down --warn-on-missing-drop

A [WARNING] line is printed for each skipped drop, and the migration is recorded as applied. Only true missing-object errors are skipped — all other errors still stop the migration.

"no migration named X"

The name passed to --to or fake does not match any registered migration. Use ./migrate dag to list exact names.


See Also