The makemigrations command is the primary command for generating Go-based database migrations from YAML schema definitions. It implements a Django-style migration workflow where each migration is a typed Go file registered in a DAG (directed acyclic graph).
The makemigrations command compares the desired schema (defined in YAML files) against the current schema (reconstructed by replaying all registered Go migration files) and generates a new .go migration file containing typed operations for each detected change.
Unlike the SQL-mode commands, Go migrations are compiled into a standalone binary (./migrations/migrate) that manages migration state, runs up/down operations, and emits the DAG structure for introspection.
makemigrations makemigrations [flags]| Flag | Type | Default | Description |
|---|---|---|---|
--check |
bool | false |
Exit with error code 1 if migrations are needed (CI/CD mode) |
--dry-run |
bool | false |
Print generated migration source without writing a file |
--merge |
bool | false |
Generate a merge migration for detected concurrent branches |
--name |
string | auto-generated | Custom name suffix for the migration file |
--verbose |
bool | false |
Show detailed pipeline output |
| Flag | Type | Default | Description |
|---|---|---|---|
--config |
string | migrations/makemigrations.config.yaml |
Path to configuration file |
The command runs a five-step pipeline each time it is invoked.
The command scans the migrations/ directory (as configured) for *.go files, excluding main.go. If no migration files exist, the current schema state is treated as empty.
When migration files exist, the command:
- Compiles all
*.gofiles in the migrations directory into a temporary binary usinggo build. - Executes
<binary> dag --format jsonto retrieveDAGOutput— a JSON structure containing:- The full migration graph (names, dependencies, operations)
- The reconstructed
SchemaState(all tables, fields, and indexes after replaying every migration in topological order) - The list of leaf migrations (the "tips" of the graph that a new migration must depend on)
- Whether the graph has branches (concurrent development)
The temporary binary is discarded after the query.
The command parses schema/schema.yaml (and any files it includes) to produce the desired schema state. This uses the same YAML parser as all other makemigrations commands.
The diff engine compares:
- Previous state: the
SchemaStatereconstructed from the DAG (or empty if no migrations exist) - Current state: the desired schema from YAML
Detected changes include table additions, removals, renames, field additions, removals, modifications, renames, and index additions and removals.
Depending on the flags:
--check: If any changes are detected, exit with error code 1. No file is written.--merge: Generate a merge migration (see Branch and Merge Workflow).- Default: Generate a new
.gomigration file in the migrations directory.
Each generated file is in package main and calls m.Register() from an init() function. This ensures the migration is automatically registered when the migrations binary starts.
// migrations/0001_initial.go
package main
import m "github.com/ocomsoft/makemigrations/migrate"
func init() {
m.Register(&m.Migration{
Name: "0001_initial",
Dependencies: []string{},
Operations: []m.Operation{
&m.CreateTable{
Name: "users",
Fields: []m.Field{
{Name: "id", Type: "uuid", PrimaryKey: true, Nullable: true},
{Name: "email", Type: "varchar", Length: 255, Nullable: true},
{Name: "created_at", Type: "timestamp", AutoCreate: true, Nullable: true},
},
Indexes: []m.Index{
{Name: "idx_users_email", Fields: []string{"email"}, Unique: true},
},
},
},
})
}migrations/NNNN_name.go
Where NNNN is a zero-padded four-digit sequence number based on the count of existing migration files, and name is either the --name flag value (lowercased, spaces replaced with underscores) or a name auto-generated from the diff content.
Examples:
migrations/0001_initial.gomigrations/0002_add_products.gomigrations/0003_rename_user_email.gomigrations/0004_merge.go(merge migration)
There are 10 typed operation types. Each operation implements Up() (forward SQL), Down() (reverse SQL), and Mutate() (updates the in-memory schema state for DAG traversal).
Creates a new database table with the specified fields and indexes.
&m.CreateTable{
Name: "products",
Fields: []m.Field{
{Name: "id", Type: "uuid", PrimaryKey: true, Nullable: true},
{Name: "name", Type: "varchar", Length: 255, Nullable: true},
{Name: "price", Type: "decimal", Precision: 10, Scale: 2, Nullable: true},
{Name: "active", Type: "boolean", Default: "true", Nullable: true},
{Name: "created_at", Type: "timestamp", AutoCreate: true, Nullable: true},
{Name: "updated_at", Type: "timestamp", AutoUpdate: true, Nullable: true},
},
Indexes: []m.Index{
{Name: "idx_products_name", Fields: []string{"name"}, Unique: false},
},
},- Destructive: No
- Down: emits
DROP TABLE
Drops an existing database table.
&m.DropTable{Name: "old_sessions"},- Destructive: Yes — all data in the table is lost
- Down: reconstructs
CREATE TABLEfrom the pre-drop schema state
Renames an existing table.
&m.RenameTable{OldName: "users", NewName: "accounts"},- Destructive: No
- Down: emits the reverse rename
Adds a new column to an existing table.
&m.AddField{
Table: "users",
Field: m.Field{
Name: "phone",
Type: "varchar",
Length: 20,
Nullable: true,
},
},- Destructive: No
- Down: emits
DROP COLUMN
Removes a column from an existing table.
&m.DropField{Table: "users", Field: "legacy_token"},- Destructive: Yes — all data in that column is lost
- Down: reconstructs
ADD COLUMNfrom the pre-drop schema state
Changes a column's type, length, nullability, default, or other constraints. Both the old and new field definitions are stored so the operation can be reversed exactly.
&m.AlterField{
Table: "users",
OldField: m.Field{Name: "status", Type: "varchar", Length: 50, Nullable: true},
NewField: m.Field{Name: "status", Type: "varchar", Length: 100, Nullable: true},
},- Destructive: No (though incompatible type changes may fail at the database level)
- Down: emits the reverse
ALTER COLUMNrestoring the old definition
Renames a column in an existing table.
&m.RenameField{Table: "users", OldName: "username", NewName: "display_name"},- Destructive: No
- Down: emits the reverse rename
Creates an index on one or more columns of an existing table.
&m.AddIndex{
Table: "orders",
Index: m.Index{
Name: "idx_orders_user_id",
Fields: []string{"user_id", "created_at"},
Unique: false,
},
},- Destructive: No
- Down: emits
DROP INDEX
Drops an index from a table.
&m.DropIndex{Table: "orders", Index: "idx_orders_legacy"},- Destructive: No (index can be recreated)
- Down: reconstructs
CREATE INDEXfrom the pre-drop schema state
Executes raw SQL directly. Used for data migrations, custom constraints, triggers, or any operation that cannot be expressed as a typed operation. RunSQL does not update the schema state.
&m.RunSQL{
ForwardSQL: "UPDATE users SET status = 'active' WHERE status IS NULL;",
BackwardSQL: "UPDATE users SET status = NULL WHERE status = 'active';",
},- Destructive: No (depends entirely on the SQL content)
- Down: executes
BackwardSQL - Note:
RunSQLoperations are not auto-generated by the diff engine. Add them manually when needed.
When the diff engine detects a destructive change (e.g. DropTable, DropField), makemigrations pauses and prompts for a decision before generating the migration:
⚠ Destructive operation detected: table_removed on "ocom_reset_password"
1) Generate — include operation in migration
2) Review — include with // REVIEW comment
3) Omit — skip operation; schema state still advances (SchemaOnly)
4) Exit — cancel migration generation
5) All — generate all remaining destructive ops without prompting
Choice [1-5]:
| Option | Effect | Generated code |
|---|---|---|
| 1) Generate | Operation is included and will run normally on migrate up |
&m.DropTable{Name: "..."} |
| 2) Review | Operation is included but preceded by a // REVIEW comment to flag for human inspection |
// REVIEW\n&m.DropTable{...} |
| 3) Omit | Operation is included with SchemaOnly: true — schema state advances but no SQL is executed |
&m.DropTable{Name: "...", SchemaOnly: true} |
| 4) Exit | Migration generation is cancelled; no file is written | — |
| 5) All | Remaining destructive operations all use option 1 without further prompting | — |
When SchemaOnly: true is set on an operation, the runner treats the table or field as already removed from the database (no SQL is executed) but updates the in-memory schema state as if it had been. This is useful when you have already manually dropped the table or field outside of migrations.
Use --silent to auto-accept all destructive operations as Generate without prompting:
makemigrations makemigrations --silentThis is equivalent to always choosing option 1. Useful in automated or non-interactive environments.
The m.Field struct supports the following properties:
| Property | Type | Description |
|---|---|---|
Name |
string | Column name (required) |
Type |
string | Column type: varchar, text, integer, bigint, boolean, uuid, timestamp, date, decimal, json, jsonb, foreign_key |
PrimaryKey |
bool | Mark as primary key |
Nullable |
bool | Allow NULL values |
Default |
string | Default value reference: "new_uuid", "now", "true", "false" |
Length |
int | Character length for varchar |
Precision |
int | Total digits for decimal/numeric |
Scale |
int | Decimal places for decimal/numeric |
AutoCreate |
bool | Automatically set on row creation (created_at pattern) |
AutoUpdate |
bool | Automatically set on row update (updated_at pattern) |
ForeignKey |
*m.ForeignKey |
Foreign key constraint |
ManyToMany |
*m.ManyToMany |
Many-to-many relationship via junction table |
m.Field{
Name: "user_id",
Type: "foreign_key",
ForeignKey: &m.ForeignKey{
Table: "users",
OnDelete: "CASCADE",
},
},# Generate a migration from detected schema changes
makemigrations makemigrations
# Output (when changes are detected)
Created migrations/0002_add_products.go
# Output (when no changes are detected)
No changes detected.makemigrations makemigrations --name "add_products"
# Generates: migrations/0002_add_products.go
makemigrations makemigrations --name "Add User Preferences"
# Generates: migrations/0003_add_user_preferences.goPreview the generated Go source without writing a file:
makemigrations makemigrations --dry-runpackage main
import m "github.com/ocomsoft/makemigrations/migrate"
func init() {
m.Register(&m.Migration{
Name: "0002_add_products",
Dependencies: []string{"0001_initial"},
Operations: []m.Operation{
&m.CreateTable{
Name: "products",
Fields: []m.Field{
{Name: "id", Type: "uuid", PrimaryKey: true, Nullable: true},
{Name: "name", Type: "varchar", Length: 255, Nullable: true},
},
},
},
})
}makemigrations makemigrations --check
# Exit codes:
# 0 — schema is up to date with all migrations
# 1 — migrations are needed or an error occurredmakemigrations makemigrations --verbose
# Output
Building migration binary from migrations/...
No changes detected.# 1. Initialise the migrations directory
makemigrations init-go
# 2. Edit the schema
vim schema/schema.yaml
# 3. Generate the first migration
makemigrations makemigrations --name "initial"
# Created migrations/0001_initial.go
# 4. Build and run the migrations binary
cd migrations && go mod tidy && go build -o migrate .
# 5. Apply migrations
./migrations/migrate up# 1. Add the 'products' table to schema/schema.yaml
# 2. Generate the migration
makemigrations makemigrations --name "add_products"
# Created migrations/0002_add_products.go
# 3. Review the generated file
cat migrations/0002_add_products.go
# 4. Rebuild the binary
cd migrations && go build -o migrate .
# 5. Apply
./migrations/migrate up# 1. Change 'status' field from varchar(50) to varchar(100) in schema/schema.yaml
# 2. Generate
makemigrations makemigrations --name "expand_user_status"
# Created migrations/0003_expand_user_status.go
# 3. Build and apply
cd migrations && go build -o migrate . && ./migrate upWhen two developers generate migrations from the same parent migration concurrently, the DAG gains two leaf nodes — a branching structure. The command detects this automatically.
makemigrations makemigrations
# Output when branches are detected
WARNING: Branches detected: 0002_add_products, 0002_add_orders
Run 'makemigrations makemigrations --merge' to generate a merge migration.A merge migration has two (or more) entries in Dependencies and an empty Operations list. It unifies the branches into a single leaf so subsequent migrations have one clear parent.
makemigrations makemigrations --merge
# Created merge migration: migrations/0003_merge_0002_add_products_and_0002_add_orders.go
# Dependencies: 0002_add_products, 0002_add_ordersThe generated file looks like:
// migrations/0003_merge_0002_add_products_and_0002_add_orders.go
package main
import m "github.com/ocomsoft/makemigrations/migrate"
func init() {
m.Register(&m.Migration{
Name: "0003_merge_0002_add_products_and_0002_add_orders",
Dependencies: []string{
"0002_add_products",
"0002_add_orders",
},
Operations: []m.Operation{},
})
}After the merge migration is committed, both branches can apply ./migrate up in any order. The merge node ensures the graph remains acyclic with a single leaf.
makemigrations makemigrations --merge --dry-run# .github/workflows/check-migrations.yml
name: Check Migrations
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.24'
- name: Install makemigrations
run: go install github.com/ocomsoft/makemigrations@latest
- name: Check for pending migrations
run: makemigrations makemigrations --check#!/bin/bash
# dev-migrate.sh
set -e
echo "Checking for schema changes..."
if makemigrations makemigrations --check 2>/dev/null; then
echo "No migrations needed"
else
echo "Generating migrations..."
makemigrations makemigrations --verbose
echo "Rebuilding migration binary..."
cd migrations && go build -o migrate .
echo "Applying migrations..."
./migrate up
echo "Done"
fiAfter initialisation and several generated migrations, the migrations/ directory looks like:
migrations/
├── go.mod # Module file: myproject/migrations
├── go.sum
├── main.go # Entry point — runs the migrate app
├── 0001_initial.go # Auto-generated
├── 0002_add_products.go
├── 0003_expand_user_status.go
└── migrate # Compiled binary (gitignored)
main.go is generated once by makemigrations init-go and must not be deleted:
package main
import (
"fmt"
"os"
m "github.com/ocomsoft/makemigrations/migrate"
)
func main() {
app := m.NewApp(m.Config{
Registry: m.GlobalRegistry(),
})
if err := app.Run(os.Args[1:]); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}go.mod is also generated once and pins the makemigrations runtime:
module myproject/migrations
go 1.24
require (
github.com/ocomsoft/makemigrations v0.3.0
)
Every time a new migration file is generated you must rebuild the binary before applying:
cd migrations && go mod tidy && go build -o migrate .
./migrations/migrate upTo verify the migration was applied:
./migrations/migrate statusTo roll back the last migration:
./migrations/migrate downTo view the full DAG:
./migrations/migrate dag
./migrations/migrate dag --format jsonThe command reads migrations/makemigrations.config.yaml:
database:
type: postgresql # Target database: postgresql, mysql, sqlite, sqlserver
migration:
directory: migrations # Where .go migration files are writtenNo schema files found
Error: parsing YAML schema: no schema files found
Create schema/schema.yaml or check the search paths.
Build failure in migrations directory
Error: querying migration DAG: building migration binary: ...
Run cd migrations && go mod tidy && go build -o migrate . manually to see the compiler error. Often caused by a missing go.sum entry after adding dependencies.
Missing dependency
Error: querying migration DAG: running dag command: migration "0003_add_orders" depends on "0002_missing" which is not registered
A migration file references a dependency that does not exist. Check the Dependencies field in the affected migration file.
Branches detected without --merge
WARNING: Branches detected: 0002_add_products, 0002_add_orders
Run 'makemigrations makemigrations --merge' to generate a merge migration.
Run with --merge to resolve.
Check mode failure
Error: migrations needed: 3 changes detected
Exit code 1. Schema and migrations are out of sync. Generate the migration and commit it.
- init-go Command — Initialise the migrations directory for Go migrations
- Schema Format Guide — Complete YAML schema reference
- Configuration Guide — Configuration options
- Architecture Guide — How the DAG and migration framework work