README
ΒΆ
Go Database Migrator
A production-ready, struct-driven PostgreSQL database migration tool. Generate accurate, safe database migrations by comparing your Go structs with your actual database schema.
Features
π Struct-Driven Migrations - Define your schema in Go structs using dbdef
tags
π₯ Advanced Engine - Uses battle-tested schema comparison for constraint normalization
π‘οΈ Safety First - Identifies destructive operations and requires explicit approval
π§ PostgreSQL Native - Built specifically for PostgreSQL with full feature support
β‘ Zero False Positives - Eliminates CHECK constraint syntax comparison issues
ποΈ Zero-Downtime - Leverages PostgreSQL's online migration capabilities
Why db-migrator?
Comparison with Alternatives
Feature | db-migrator | golang-migrate | Atlas | GORM AutoMigrate |
---|---|---|---|---|
Struct-driven | β | β | β | β |
Advanced schema comparison | β | β | β | β |
Down migrations | β | β | β | β |
Safety checks | β | β | β | β |
Zero false positives | β | β | β | β |
Zero-downtime migrations | β | β | β | β |
Quick Start
Installation
go install github.com/eleven-am/db-migrator@latest
Basic Usage
- Define your models with
dbdef
tags:
package models
import "time"
type User struct {
// Table-level configuration
_ struct{} `dbdef:"table:users;index:idx_users_team_id,team_id"`
ID string `db:"id" dbdef:"type:cuid;primary_key;default:gen_cuid()"`
Email string `db:"email" dbdef:"type:varchar(255);not_null;unique"`
TeamID string `db:"team_id" dbdef:"type:cuid;not_null;foreign_key:teams.id"`
IsActive bool `db:"is_active" dbdef:"type:boolean;not_null;default:true"`
CreatedAt time.Time `db:"created_at" dbdef:"type:timestamptz;not_null;default:now()"`
}
- Generate migrations:
db-migrator migrate \
--url="postgres://user:pass@localhost/mydb" \
--package="./internal/models" \
--output="./migrations"
- Choose your workflow:
- File-based: Generate migration files for review and manual application
- Direct execution: Use
--push
to apply changes immediately - Preview mode: Use
--dry-run
to see changes without applying
Reverse Engineering (Database-First)
The introspect
command with --format=go
enables database-first development by generating Go structs from your existing database schema:
db-migrator introspect --database="postgres://localhost/mydb" --format=go --output=models.go
This generates:
- Go structs for all tables with proper field types
- Complete
dbdef
tags matching your database schema - Foreign key relationships with proper references
- Indexes and constraints preserved in table-level tags
- Enum types as Go constants
Example Generated Code
From a users
table with foreign keys and indexes:
package models
import (
"time"
)
// User represents the users table
type User struct {
_ struct{} `dbdef:"table:users;index:idx_users_email,email;unique:uk_users_username,username"`
ID string `db:"id" dbdef:"type:uuid;primary_key;default:gen_random_uuid()"`
Email string `db:"email" dbdef:"type:varchar(255);not_null"`
Username string `db:"username" dbdef:"type:varchar(100);not_null;unique"`
TeamID string `db:"team_id" dbdef:"type:uuid;not_null;foreign_key:teams.id;on_delete:CASCADE"`
Role string `db:"role" dbdef:"type:varchar(50);not_null;default:'member'"`
IsActive bool `db:"is_active" dbdef:"type:boolean;not_null;default:true"`
CreatedAt time.Time `db:"created_at" dbdef:"type:timestamptz;not_null;default:now()"`
UpdatedAt time.Time `db:"updated_at" dbdef:"type:timestamptz;not_null;default:now()"`
}
This allows you to:
- Start with an existing database
- Generate Go structs that match exactly
- Use db-migrator for future schema changes
- Maintain consistency between code and database
Architecture
How Tag Parsing Works
The db-migrator
leverages Go's powerful reflect
package to inspect your struct definitions at runtime. When you specify the --package
flag, the tool:
- Scans Go Files: It reads all
.go
files within the specified package path. - Identifies Structs: It identifies
struct
types that are intended to represent database tables. - Parses
dbdef
Tags: For each field in these structs, and for the struct itself (for table-level tags), it reads and parses thedbdef
string tag. These tags are simple key-value pairs (e.g.,type:varchar(255)
,primary_key
) separated by semicolons. - Builds Internal Schema: The parsed information is then used to construct an in-memory representation of your desired database schema, which is later compared against the actual database schema.
This approach allows you to define your database schema directly alongside your Go models, ensuring consistency and reducing the need for separate schema definition files.
Smart Struct-Driven Migration Strategy
The tool implements a "Smart Struct-Driven Migration" approach:
- Parse Go structs with
dbdef
tags to understand desired schema - Introspect current PostgreSQL database schema
- Compare using normalized, signature-based matching
- Generate safe SQL migrations with rollback support
Signature-Based Comparison
Instead of comparing by names (which can differ), we use semantic signatures:
// These are considered identical:
STRUCT: idx_users_email -> table:users|cols:email|unique:true|method:btree
DATABASE: users_email_key -> table:users|cols:email|unique:true|method:btree
This eliminates false positives from naming differences while ensuring semantic accuracy.
DBDef Tag Syntax
Comprehensive dbdef
Example
Here's an example combining various field and table-level attributes:
package models
import "time"
type Product struct {
// Table-level configuration: custom table name, composite unique constraint, and a partial index
_ struct{} `dbdef:"table:store_products;unique:uk_product_sku_vendor,sku,vendor_id;index:idx_active_products,status where:status='active'"`
ID string `db:"id" dbdef:"type:uuid;primary_key;default:gen_random_uuid()"`
Name string `db:"name" dbdef:"type:varchar(255);not_null"`
SKU string `db:"sku" dbdef:"type:varchar(100);not_null"`
VendorID string `db:"vendor_id" dbdef:"type:uuid;not_null;foreign_key:vendors.id;on_delete:CASCADE"`
Price float64 `db:"price" dbdef:"type:numeric(10,2);not_null;default:0.00"`
Description *string `db:"description" dbdef:"type:text"` // Nullable field
Status string `db:"status" dbdef:"type:varchar(50);not_null;default:'draft'"`
Metadata map[string]interface{} `db:"metadata" dbdef:"type:jsonb;default:'{}'"`
CreatedAt time.Time `db:"created_at" dbdef:"type:timestamptz;not_null;default:now()"`
UpdatedAt time.Time `db:"updated_at" dbdef:"type:timestamptz;not_null;default:now()"`
}
// Vendor struct for foreign key reference
type Vendor struct {
_ struct{} `dbdef:"table:vendors"`
ID string `db:"id" dbdef:"type:uuid;primary_key;default:gen_random_uuid()"`
Name string `db:"name" dbdef:"type:varchar(255);not_null;unique"`
}
Field-Level Tags
type Example struct {
ID string `db:"id" dbdef:"type:cuid;primary_key;default:gen_cuid()"`
Email string `db:"email" dbdef:"type:varchar(255);not_null;unique"`
TeamID string `db:"team_id" dbdef:"type:cuid;foreign_key:teams.id;on_delete:CASCADE"`
IsActive bool `db:"is_active" dbdef:"type:boolean;default:true"`
}
Supported field attributes:
type:
- PostgreSQL data type (varchar, integer, boolean, jsonb, etc.)primary_key
- Mark as primary keyunique
- Add unique constraintnot_null
- NOT NULL constraintdefault:
- Default valueforeign_key:
- Foreign key reference (table.column)on_delete:
,on_update:
- FK actions (CASCADE, RESTRICT, SET NULL)
Table-Level Tags
type Example struct {
_ struct{} `dbdef:"table:examples;index:idx_name,column1,column2;unique:uk_name,col1,col2;index:idx_partial,status where:status='active'"`
// ... fields
}
Supported table attributes:
table:
- Override table name (defaults to snake_case plural)index:
- Create regular index (name,col1,col2
)unique:
- Create unique constraint (name,col1,col2
)where:
- Add WHERE clause for partial indexes
Workflow Modes
The db-migrator supports three distinct workflow modes to fit different development needs:
1. File-Based Workflow (Default)
Generate migration files for review and version control:
db-migrator migrate --url="postgres://localhost/mydb" --package="./models"
Benefits:
- Migration files are version controlled
- Enables code review of schema changes
- Compatible with existing migration tools
- Safe rollback with down migrations
2. Direct Execution Workflow
Apply schema changes immediately with --push
:
db-migrator migrate --url="postgres://localhost/mydb" --package="./models" --push
Benefits:
- Instant schema updates during development
- Single command for generate + apply
- No intermediate files to manage
- Perfect for rapid iteration
3. Preview Workflow
Review changes without applying them using --dry-run
:
db-migrator migrate --url="postgres://localhost/mydb" --package="./models" --dry-run
Benefits:
- Understand what changes will be made
- Validate schema differences
- Debug struct definitions
- Safe exploration of schema changes
Troubleshooting
Common Issues and Solutions
-
pq: password authentication failed for user "..."
:- Cause: Incorrect database username or password.
- Solution: Double-check your
--user
and--password
flags, or the credentials in your--url
. Ensure the database user has the necessary permissions.
-
dial tcp ...: connect: connection refused
:- Cause: The PostgreSQL database is not running or is not accessible at the specified host and port.
- Solution: Verify that your PostgreSQL server is running and listening on the correct port (
--port
). Check firewall rules if connecting remotely.
-
ERROR: relation "..." does not exist
:- Cause: Your Go struct defines a foreign key or references a table that does not exist in the target database.
- Solution: Ensure all referenced tables exist. If you're creating a new schema, run an initial migration to create base tables.
-
Failed to parse directory: ... no Go files found
:- Cause: The
--package
path does not contain any.go
files or the path is incorrect. - Solution: Verify the
--package
flag points to a directory containing your Go model structs.
- Cause: The
-
Migration generated but not applied
:- Cause: You ran
db-migrator migrate
without the--push
flag. - Solution: The default behavior is to generate
.up.sql
and.down.sql
files for manual review. To apply directly, use--push
. To see the SQL without applying, use--dry-run
.
- Cause: You ran
-
Destructive operation detected: DROP TABLE ...
:- Cause: The tool detected a schema change that would result in data loss (e.g., dropping a table or column).
- Solution: By default,
db-migrator
prevents destructive operations. If you intend to perform such an operation, use the--allow-destructive
flag. Use with caution!
CLI Commands
migrate
Generate database migrations by comparing Go structs with database schema.
db-migrator migrate [flags]
Connection Options:
--url string # Full connection URL
--host string # Database host (default "localhost")
--port string # Database port (default "5432")
--user string # Database user
--password string # Database password
--dbname string # Database name
--sslmode string # SSL mode (default "disable")
Migration Options:
--package string # Go package path (default "./internal/db")
--output string # Migration output directory (default "./migrations")
--name string # Migration name prefix
--dry-run # Print changes without creating files
--push # Execute SQL directly on database
--allow-destructive # Allow DROP operations
--create-if-not-exists # Create database if missing
introspect
Inspect database schema and export in various formats. This is a read-only operation that analyzes your database structure without making any changes.
db-migrator introspect [flags]
Options:
--database, -d string # Database connection URL (required)
--format, -f string # Export format: json, yaml, markdown, sql, dot (default "markdown")
--output, -o string # Output file (default: stdout)
--table, -t string # Inspect specific table only
--schema, -s string # Database schema to inspect (default "public")
Export Formats:
json
- Machine-readable JSON formatyaml
- YAML format for configurationmarkdown
- Human-readable documentationsql
- SQL DDL statementsdot
- GraphViz format for visualizationgo
- Generate Go structs with dbdef tags
Examples
# Basic migration
db-migrator migrate --url="postgres://localhost/mydb" --package="./models"
# With custom output directory
db-migrator migrate \
--host="localhost" \
--user="postgres" \
--dbname="myapp" \
--package="./internal/models" \
--output="./db/migrations"
# Dry run to preview changes
db-migrator migrate --url="postgres://localhost/mydb" --package="./models" --dry-run
# Allow destructive operations
db-migrator migrate --url="postgres://localhost/mydb" --package="./models" --allow-destructive
# Execute migration directly on database
db-migrator migrate --url="postgres://localhost/mydb" --package="./models" --push
# Generate and execute in one command
db-migrator migrate --url="postgres://localhost/mydb" --package="./models" --push --allow-destructive
# Introspect database and output as markdown documentation
db-migrator introspect --database="postgres://localhost/mydb" --format=markdown
# Export schema as JSON to file
db-migrator introspect --database="postgres://localhost/mydb" --format=json --output=schema.json
# Generate GraphViz visualization
db-migrator introspect --database="postgres://localhost/mydb" --format=dot --output=schema.dot
# Then: dot -Tpng schema.dot -o schema.png
# Inspect specific table only
db-migrator introspect --database="postgres://localhost/mydb" --table=users --format=yaml
# Export SQL DDL for entire schema
db-migrator introspect --database="postgres://localhost/mydb" --format=sql --output=schema.sql
# Generate Go structs from database (reverse engineering)
db-migrator introspect --database="postgres://localhost/mydb" --format=go --output=models.go
# Generate Go structs with custom package name
db-migrator introspect --database="postgres://localhost/mydb" --format=go --package=db --output=internal/db/models.go
Common Patterns
Soft Deletes
Add soft delete functionality with partial indexes:
type Model struct {
DeletedAt *time.Time `db:"deleted_at" dbdef:"type:timestamptz"`
_ struct{} `dbdef:"index:idx_not_deleted,deleted_at where:deleted_at IS NULL"`
}
Audit Fields
Track who created and modified records:
type Auditable struct {
CreatedBy string `db:"created_by" dbdef:"type:uuid;not_null;foreign_key:users.id"`
UpdatedBy string `db:"updated_by" dbdef:"type:uuid;not_null;foreign_key:users.id"`
CreatedAt time.Time `db:"created_at" dbdef:"type:timestamptz;not_null;default:now()"`
UpdatedAt time.Time `db:"updated_at" dbdef:"type:timestamptz;not_null;default:now()"`
}
Multi-tenant Patterns
Implement row-level security with tenant isolation:
type TenantModel struct {
_ struct{} `dbdef:"table:products;index:idx_tenant_active,tenant_id,is_active"`
TenantID string `db:"tenant_id" dbdef:"type:uuid;not_null;foreign_key:tenants.id"`
// Composite unique constraint across tenant
_ struct{} `dbdef:"unique:uk_tenant_sku,tenant_id,sku"`
}
Versioning
Track record versions for audit trails:
type Versioned struct {
Version int `db:"version" dbdef:"type:integer;not_null;default:1"`
UpdatedAt time.Time `db:"updated_at" dbdef:"type:timestamptz;not_null;default:now()"`
_ struct{} `dbdef:"index:idx_version,entity_id,version"`
}
Advanced Features
Partial Indexes
Create conditional indexes with WHERE clauses:
type Order struct {
_ struct{} `dbdef:"table:orders;index:idx_active_orders,status where:status='active'"`
Status string `db:"status" dbdef:"type:varchar(50);not_null"`
}
Composite Indexes
Create multi-column indexes:
type AuditLog struct {
_ struct{} `dbdef:"table:audit_logs;index:idx_entity,entity_type,entity_id;unique:uk_audit,user_id,action,created_at"`
EntityType string `db:"entity_type" dbdef:"type:varchar(50);not_null"`
EntityID string `db:"entity_id" dbdef:"type:cuid;not_null"`
}
Foreign Key Actions
Specify CASCADE, RESTRICT, or SET NULL behavior:
type Project struct {
TeamID string `db:"team_id" dbdef:"type:cuid;foreign_key:teams.id;on_delete:CASCADE;on_update:RESTRICT"`
OwnerID *string `db:"owner_id" dbdef:"type:cuid;foreign_key:users.id;on_delete:SET NULL"`
}
JSONB Support
Full support for PostgreSQL JSONB columns:
type Config struct {
Metadata JSONField[map[string]interface{}] `db:"metadata" dbdef:"type:jsonb;default:'{}'"`
Settings JSONField[AppSettings] `db:"settings" dbdef:"type:jsonb;not_null"`
}
Safety & Validation
Destructive Operation Detection
The tool automatically identifies potentially dangerous operations:
- β DROP CONSTRAINT (unique, foreign key)
- β DROP INDEX (unique indexes)
- β CREATE INDEX (always safe)
- β CREATE CONSTRAINT (always safe)
- β DROP INDEX (non-unique regular indexes)
Migration Safety Levels
- Safe Mode (default) - Only generates safe operations
- Destructive Mode (
--allow-destructive
) - Includes DROP operations - Dry Run Mode (
--dry-run
) - Shows changes without creating files
Generated Migration Files
migrations/
βββ 20240101120000_schema_update.up.sql # Forward migration
βββ 20240101120000_schema_update.down.sql # Rollback migration
Integration Examples
With sqlx and squirrel
db-migrator works seamlessly with popular Go database libraries:
// Define your schema with db-migrator tags
type User struct {
_ struct{} `dbdef:"table:users;index:idx_email,email"`
ID string `db:"id" dbdef:"type:uuid;primary_key;default:gen_random_uuid()"`
Email string `db:"email" dbdef:"type:varchar(255);not_null;unique"`
TeamID string `db:"team_id" dbdef:"type:uuid;not_null;foreign_key:teams.id"`
CreatedAt time.Time `db:"created_at" dbdef:"type:timestamptz;not_null;default:now()"`
}
// Use with sqlx - the db tags work perfectly
var user User
err := db.Get(&user, "SELECT * FROM users WHERE id = $1", userID)
// Use with squirrel - clean query building
query, args, _ := sq.Select("*").
From("users").
Where(sq.Eq{"team_id": teamID}).
OrderBy("created_at DESC").
ToSql()
With golang-migrate
# Generate migrations
db-migrator migrate --url="postgres://localhost/mydb" --package="./models"
# Apply with golang-migrate
migrate -path ./migrations -database "postgres://localhost/mydb" up
With Atlas
# Generate schema from structs
db-migrator migrate --dry-run --package="./models" > schema.sql
# Use with Atlas
atlas migrate diff --to "file://schema.sql"
In CI/CD Pipeline
name: Database Migration Check
on: [push, pull_request]
jobs:
migration-check:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: '1.19'
- name: Install db-migrator
run: go install ./tools/db-migrator
- name: Check for schema changes
run: |
db-migrator migrate \
--url="postgres://postgres:postgres@localhost/postgres" \
--package="./internal/models" \
--dry-run
Performance
Benchmarks
The tool is optimized for performance with real-world databases:
BenchmarkIntrospection-8 100 req/sec 50ms per operation
BenchmarkNormalization-8 10000 req/sec 0.1ms per operation
BenchmarkComparison-8 1000 req/sec 5ms per operation
Scalability
Tested with:
- β 100+ tables
- β 1000+ indexes
- β Complex schema hierarchies
- β Large WHERE clauses
Testing
Comprehensive test suite with 95%+ coverage:
# Run all tests
make test
# Unit tests only (no database required)
make test-unit
# Integration tests (requires PostgreSQL)
make test-integration
# Generate coverage report
make test-coverage
See TEST_DOCUMENTATION.md for detailed testing information.
Contributing
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature
) - Write tests for your changes
- Ensure all tests pass (
make test
) - Commit your changes (
git commit -m 'Add amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
Development Setup
# Clone repository
git clone https://github.com/eleven-am/db-migrator.git
cd db-migrator
# Install dependencies
go mod download
# Start PostgreSQL (for integration tests)
docker run --name postgres-test -e POSTGRES_PASSWORD=postgres -p 5432:5432 -d postgres:15
# Run tests
make test
Current Limitations
- PostgreSQL only - MySQL support is on the roadmap
- No stored procedures/functions - Only tables, indexes, and constraints are supported
- No custom types - PostgreSQL domains and custom types not yet supported
- Forward-only migrations - No automatic rollback verification
- No check constraints - Check constraints parsing not implemented
- No triggers - Database triggers are not managed
Roadmap
- MySQL support
- Schema validation rules
- Migration rollback verification
- Parallel migration execution
- Check constraints support
- Custom types (domains)
- Stored procedures/functions
License
This project is licensed under the MIT License - see the LICENSE file for details.
Support
- π Documentation: GitHub Wiki
- π Bug Reports: GitHub Issues
- π¬ Discussions: GitHub Discussions
- π§ Email: roy@theossaibrothers.com
Acknowledgments
- PostgreSQL team for excellent database features
- Go community for robust tooling
- pg_query_go for SQL parsing
- All contributors and testers
Built with β€οΈ by Roy OSSAI for the Go and PostgreSQL communities.
Documentation
ΒΆ
There is no documentation for this package.