Skip to content

Migration File Structure — Complete Guide

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Migration File Structure. We cover key concepts, practical examples, and best practices to help you master this topic.

Learn migration file structure: naming conventions, upgrade/downgrade functions, metadata headers, dependency declarations, and organizing migration files for maintainability across teams.

What You Learn

You will learn how to structure migration files properly: naming conventions that prevent conflicts, upgrade and downgrade function patterns, metadata for tracking, dependency declarations for ordered execution, and organizational best practices.

Why It Matters

Well-structured migration files are maintainable over years of development. Poorly structured files cause naming conflicts, ordering issues, and confusion about what each migration does. Consistent structure makes migrations reviewable and reliable.

Real-World Use

DodaTech's Django application has 847 migrations across 15 apps. Each migration follows a strict naming convention and structure. New developers can understand any migration by its file name and header. Migration reviews take minutes instead of hours.

Naming Convention

# Timestamp-based naming (recommended)
# Format: YYYYMMDD_HHMMSS_description.py
#
# 20260628_120000_add_phone_to_users.py
# 20260628_130000_create_orders_table.py
# 20260628_140000_add_foreign_key_to_orders.py

# Sequential naming (simple projects)
# 001_add_phone_to_users.py
# 002_create_orders_table.py
# 003_add_foreign_key_to_orders.py

# Description guidelines
# GOOD: 20260628_120000_add_phone_to_users.py
# GOOD: 20260628_130000_create_orders_table.py
# BAD:  20260628_120000_fix.py (describes what, not why)
# BAD:  20260628_130000_migration.py (duplicate name risk)

def get_migration_name(description):
    timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
    safe_desc = description.lower().replace(' ', '_').replace('-', '_')[:50]
    return f'{timestamp}_{safe_desc}'

Expected output: Migration names include a timestamp or sequence number and a description. Descriptions are concise and descriptive. Names are unique and ordered chronologically.

Alembic Migration Structure

"""add phone column to users table

Revision ID: abc123def456
Revises: prev_revision_id
Create Date: 2026-06-28 12:00:00.000000
"""

from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# Revision identifiers
revision = 'abc123def456'
down_revision = 'prev_revision_id'
branch_labels = None
depends_on = None

def upgrade():
    op.add_column('users',
        sa.Column('phone', sa.String(20), nullable=True)
    )
    op.create_index('idx_users_phone', 'users', ['phone'])

def downgrade():
    op.drop_index('idx_users_phone', table_name='users')
    op.drop_column('users', 'phone')

Expected output: Alembic migration includes a docstring with description, revision ID, parent revision, and timestamp. The upgrade() function applies changes. The downgrade() function reverts them.

Flyway Migration Structure

-- Flyway SQL migration
-- File: V20260628_120000__add_phone_to_users.sql
-- Version: V (versioned), U (undo), R (repeatable)

-- Description: Add phone column to users table
-- Author: developer@example.com
-- Date: 2026-06-28

-- UP migration
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
CREATE INDEX idx_users_phone ON users (phone);

-- Note: Flyway does not support undo automatically.
-- Use separate U migration files for rollback.
-- Flyway undo migration
-- File: U20260628_120000__add_phone_to_users.sql

-- DOWN migration
DROP INDEX IF EXISTS idx_users_phone;
ALTER TABLE users DROP COLUMN IF EXISTS phone;

Expected output: Flyway uses SQL files with version prefix (V), description, and double-underscore separator. Undo migrations use U prefix. Repeatable migrations use R prefix and are re-applied when checksum changes.

Knex Migration Structure

// Knex migration
// File: 20260628120000_add_phone_to_users.js

exports.up = function(knex) {
    return knex.schema.table('users', function(table) {
        table.string('phone', 20).nullable();
        table.index('phone', 'idx_users_phone');
    });
};

exports.down = function(knex) {
    return knex.schema.table('users', function(table) {
        table.dropIndex('phone', 'idx_users_phone');
        table.dropColumn('phone');
    });
};

Expected output: Knex migration exports up and down functions. Each function receives the knex instance. Schema Builder methods make migrations database-agnostic. Migrations are ordered by timestamp prefix.

Django Migration Structure

# Django auto-generated migration
from django.db import migrations, models

class Migration(migrations.Migration):
    initial = True  # First migration for this app

    dependencies = [
        ('users', '0001_initial'),
    ]

    operations = [
        migrations.AddField(
            model_name='user',
            name='phone',
            field=models.CharField(max_length=20, blank=True, null=True),
        ),
        migrations.AddIndex(
            model_name='user',
            index=models.Index(fields=['phone'], name='idx_users_phone'),
        ),
    ]

Expected output: Django migrations define dependencies on previous migrations and list operations. Operations are ORM-agnostic. Django auto-generates migrations from model changes.

Migration Organization

const migrationDirectoryStructure = {
    // Good: flat directory with all migrations
    'migrations/': [
        '20260628_120000_add_phone_to_users.py',
        '20260628_130000_create_orders_table.py',
        '20260628_140000_add_foreign_key.py',
    ],

    // Good: separated by schema/app
    'migrations/users/': [
        '20260628_120000_add_phone.py',
    ],
    'migrations/orders/': [
        '20260628_130000_create_table.py',
    ],

    // Grouped by release
    'migrations/releases/': [
        'v2.0/',
        'v2.1/',
    ],
};

Expected output: Migrations are organized in a flat directory or separated by schema/app. Flat directories are simpler. Schema-separated directories scale better for large projects with many schemas.

Common Mistakes

1. Unclear Migration Descriptions

A migration named fix.py or migration.py requires opening the file to understand what it does. Use descriptive names: add_phone_to_users, create_orders_table. Descriptions should describe the change.

2. Missing Dependency Declaration

If migration B depends on table created in migration A, but B does not declare A as a dependency, B may run before A. Always declare dependencies. Migration tools enforce ordered execution based on dependencies.

3. Mixing Schema and Data Changes in One Migration

Schema changes and data migrations have different failure modes. Schema changes are fast and transactional. Data migrations are slow and may need batching. Separate them into different migration files.

4. Not Committing Migration Files

Migration files in .gitignore are invisible to the team. Each team member must independently create the same migrations. Always commit migration files. They are part of the codebase.

5. Editing Applied Migrations

Editing a migration that has already been applied changes its hash. Migration tools detect the change and refuse to proceed. Create a new migration for the correction instead.

Practice Questions

1. What is the recommended naming convention for migration files?

Timestamp-based: YYYYMMDD_HHMMSS_description.py. Timestamps prevent conflicts, ensure ordering, and provide chronological history. Descriptions should be concise and descriptive.

2. Why do migrations need both upgrade and downgrade functions?

Upgrade applies the change. Downgrade reverts it. The downgrade enables rollback to the previous schema version. Without it, rollback requires manual reverse engineering.

3. How do migration tools know the order to apply migrations?

Through dependency declarations. Each migration references the previous migration it depends on. The tool builds a dependency graph and applies migrations in the correct order.

4. What is the difference between versioned and repeatable migrations (Flyway)?

Versioned migrations (V prefix) are applied once in order. Repeatable migrations (R prefix) are re-applied when content changes. Use versioned for schema changes. Use repeatable for views, functions, and procedures.

Challenge

Design a migration file structure for a multi-service application with: shared schema (users, accounts), service-specific schemas (orders, payments, analytics), repeatable migrations for views and functions, CI/CD integration requiring dependency ordering, and support for 10+ developers working concurrently.

FAQ

Should I use sequential or timestamp migration naming?

Timestamp-based is recommended for team projects. Sequential works for solo projects or when migrations are merged centrally. Timestamps avoid conflicts when multiple developers create migrations concurrently.

What goes in the migration file header/docstring?

Description of the change, revision ID, parent revision, creation timestamp, author, and optional notes about data migration or locking concerns. This metadata aids debugging and auditing.

How do I handle migration dependencies across apps?

Django uses app-level dependencies. Alembic uses down_revision chains. Cross-app dependencies reference migrations from other apps. Ensure dependent migrations are applied before their dependents.

Should each table have its own migration directory?

For large projects with hundreds of tables, schema-separated directories (migrations/users/, migrations/orders/) improve organization. For most projects, a flat migration directory is sufficient.

Can I rename a migration file after it is applied?

No. The migration tool tracks applied migrations by filename or revision ID. Renaming breaks the tracking. If you must rename, create a new migration that documents the change.

Mini Project: Migration Structure Setup

Set up migration files for a new application with: Alembic initialization, three migration files (users table, orders table, foreign key), proper naming convention, complete docstrings and metadata, upgrade and downgrade functions, and dependency declarations between migrations.

What's Next

Now that you understand migration file structure, learn about Up and Down Methods for writing effective migration operations.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro