Skip to main content

Migrations

Manage database schema changes with Goose migrations.

Directory Structure

internal/db/
├── migrations/ # SQL migration files
├── queries/ # SQLC query files
└── generated/ # SQLC generated code

Creating Migrations

make migrate-create NAME=add_posts_table

Creates: internal/db/migrations/20240115143022_add_posts_table.sql

File Format

-- +goose Up
CREATE TABLE posts (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
author_id TEXT NOT NULL REFERENCES users(id),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- +goose Down
DROP TABLE IF EXISTS posts;

Running Migrations

Make targets:

make migrate-up       # Apply pending
make migrate-down # Roll back last
make migrate-status # Check status

CLI (PostgreSQL only):

tracks db migrate            # Apply pending
tracks db migrate --dry-run # Preview
tracks db rollback # Roll back last
tracks db status # Check status
tracks db reset --force # Reset database

Best Practices

Always include down migrations:

-- +goose Up
ALTER TABLE users ADD COLUMN bio TEXT;

-- +goose Down
ALTER TABLE users DROP COLUMN bio;

Use idempotent statements:

CREATE TABLE IF NOT EXISTS posts (...);
DROP TABLE IF EXISTS posts;

One change per migration - keep migrations focused on a single logical change.

Test both directions before deploying:

make migrate-up && make migrate-down && make migrate-up

Driver-Specific Notes

PostgreSQL

  • Transactional DDL (atomic schema changes)
  • Supports CREATE INDEX CONCURRENTLY

SQLite

Limited ALTER TABLE support. To drop a column, recreate the table:

-- +goose Up
CREATE TABLE posts_new (...);
INSERT INTO posts_new SELECT ... FROM posts;
DROP TABLE posts;
ALTER TABLE posts_new RENAME TO posts;

Troubleshooting

Dirty database: Migration failed midway. Check status, manually fix, then update goose_db_version table.

Order conflicts: Coordinate timestamps when multiple developers create migrations. Rebase before merging.