Skip to content
Back to skills

Migration Safety Checker

ASecurity

This skill should be used when the user asks to "create a migration", "run makemigrations", "modify a model field", "rename a column", or mentions "schema change", "alter table", "database migration". Validates migrations are production-safe.

  • 6 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added May 28, 2026
ai-agentspythongobashsqldjangotestinggitdatabaseci/cd

Security analysis

A100/100

Scanned May 28, 2026

npx -y skills add smicolon/ai-kit --skill migration-safety-checker --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Migration Safety Checker?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Migration Safety Checker
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/smicolon-migration-safety-checker/badge)](https://www.skillsdirectory.com/skills/smicolon-migration-safety-checker)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

SKILL.md
---
name: migration-safety-checker
description: This skill should be used when the user asks to "create a migration", "run makemigrations", "modify a model field", "rename a column", or mentions "schema change", "alter table", "database migration". Validates migrations are production-safe.
---

# Migration Safety Checker

Validates Django migrations are production-safe before they cause data loss.

## Activation Triggers

This skill activates when:
- Running `makemigrations`
- Modifying model fields
- Mentioning "migration", "schema", "alter table"
- Adding/removing/renaming fields
- Changing field types
- Creating or modifying migration files

## Safety Requirements

All migrations MUST be:
- ✅ **Reversible** - Can roll back safely
- ✅ **No data loss** - Preserve existing data
- ✅ **Tested** - Run on staging first
- ✅ **Safe for production** - No downtime operations
- ✅ **Documented** - Explain what changes and why

## High-Risk Operations

These operations require special attention:

### 🔴 CRITICAL RISK - Data Loss Possible

1. **Dropping columns** without data migration
2. **Renaming fields** without data migration
3. **Changing field types** (data conversion issues)
4. **Adding non-nullable fields** without default
5. **Removing tables** with data

### 🟡 MEDIUM RISK - Requires Careful Planning

1. **Adding indexes** on large tables (use `CONCURRENTLY`)
2. **Adding unique constraints** (check for duplicates first)
3. **Changing `max_length`** (data truncation risk)

### 🟢 LOW RISK - Generally Safe

1. **Adding nullable fields**
2. **Adding fields with defaults**
3. **Creating new tables**
4. **Adding non-unique indexes**

## Validation Process

### Step 1: Analyze Migration File

When migration is created:

```python
# Generated migration
class Migration(migrations.Migration):
    operations = [
        migrations.RemoveField(
            model_name='user',
            name='old_email',  # 🔴 DATA LOSS RISK!
        ),
    ]
```

### Step 2: Identify Risks

Detect:
- Operation type: `RemoveField`
- Field: `old_email`
- **Risk:** 🔴 CRITICAL - Dropping column loses data!

### Step 3: Block Unsafe Migration

> **⛔ UNSAFE MIGRATION DETECTED**
>
> **Operation:** `RemoveField(model_name='user', name='old_email')`
>
> **Risk:** 🔴 CRITICAL - Data loss!
>
> **Problem:**
> - Drops `old_email` column immediately
> - All existing email data will be PERMANENTLY LOST
> - Cannot be rolled back after data is gone
>
> **Safe Approach:**
> Use 3-step migration pattern:
> 1. Data migration: Archive `old_email` data
> 2. Wait for deployment
> 3. Drop column in separate migration
>
> Should I generate the safe migration sequence?

### Step 4: Generate Safe Migration

If user approves, create:

```python
# Migration 1: Archive data
class Migration(migrations.Migration):
    dependencies = [('users', '0001_initial')]

    operations = [
        migrations.RunPython(archive_old_emails, reverse_code=restore_old_emails),
    ]

def archive_old_emails(apps, schema_editor):
    """Archive old_email data before dropping."""
    User = apps.get_model('users', 'User')
    EmailArchive = apps.get_model('users', 'EmailArchive')

    for user in User.objects.all():
        if user.old_email:
            EmailArchive.objects.create(
                user_id=user.id,
                email=user.old_email
            )

def restore_old_emails(apps, schema_editor):
    """Restore old_email data on rollback."""
    User = apps.get_model('users', 'User')
    EmailArchive = apps.get_model('users', 'EmailArchive')

    for archive in EmailArchive.objects.all():
        User.objects.filter(id=archive.user_id).update(
            old_email=archive.email
        )

# Migration 2: Drop column (run after deployment)
class Migration(migrations.Migration):
    dependencies = [('users', '0002_archive_emails')]

    operations = [
        migrations.RemoveField(
            model_name='user',
            name='old_email',
        ),
    ]
```

## Safe Migration Patterns

### Pattern 1: Renaming Field (3 Steps)

**❌ UNSAFE:**
```python
operations = [
    migrations.RenameField('user', 'email', 'email_address'),  # ❌ Risky!
]
```

**✅ SAFE (3-Step):**
```python
# Step 1: Add new field
operations = [
    migrations.AddField('user', 'email_address',
                       models.EmailField(null=True))
]

# Step 2: Data migration (copy old → new)
operations = [
    migrations.RunPython(copy_email_to_email_address)
]

# Step 3: Drop old field (separate deployment)
operations = [
    migrations.RemoveField('user', 'email')
]
```

### Pattern 2: Changing Field Type (3 Steps)

**❌ UNSAFE:**
```python
operations = [
    migrations.AlterField('product', 'price',
                         models.DecimalField(max_digits=10, decimal_places=2))
]
```

**✅ SAFE:**
```python
# Step 1: Add new field with new type
operations = [
    migrations.AddField('product', 'price_decimal',
                       models.DecimalField(max_digits=10, decimal_places=2, null=True))
]

# Step 2: Data migration (convert & copy)
operations = [
    migrations.RunPython(convert_price_to_decimal)
]

# Step 3: Drop old field, rename new field
operations = [
    migrations.RemoveField('product', 'price'),
    migrations.RenameField('product', 'price_decimal', 'price')
]
```

### Pattern 3: Adding Non-Nullable Field (2 Steps)

**❌ UNSAFE:**
```python
operations = [
    migrations.AddField('user', 'organization',
                       models.ForeignKey('Organization'))  # ❌ No default!
]
```

**✅ SAFE:**
```python
# Step 1: Add as nullable
operations = [
    migrations.AddField('user', 'organization',
                       models.ForeignKey('Organization', null=True))
]

# Step 2: Populate + make non-nullable
operations = [
    migrations.RunPython(assign_default_organizations),
    migrations.AlterField('user', 'organization',
                         models.ForeignKey('Organization', null=False))
]
```

### Pattern 4: Adding Unique Constraint (Check First)

**❌ UNSAFE:**
```python
operations = [
    migrations.AlterField('user', 'email',
                         models.EmailField(unique=True))  # ❌ Fails if duplicates!
]
```

**✅ SAFE:**
```python
# Step 1: Check for duplicates
operations = [
    migrations.RunPython(check_and_fix_duplicate_emails)
]

# Step 2: Add unique constraint
operations = [
    migrations.AlterField('user', 'email',
                         models.EmailField(unique=True))
]
```

## Production-Safe Practices

### Use CONCURRENT Index Creation

```python
from django.db import migrations
from django.contrib.postgres.operations import AddIndexConcurrently

class Migration(migrations.Migration):
    atomic = False  # Required for CONCURRENT operations

    operations = [
        AddIndexConcurrently(
            model_name='user',
            index=models.Index(fields=['email'], name='user_email_idx')
        )
    ]
```

### Data Migration Template

```python
def forward_migration(apps, schema_editor):
    """
    Safe data migration with batching.
    """
    Model = apps.get_model('app', 'Model')
    batch_size = 1000

    total = Model.objects.count()
    for offset in range(0, total, batch_size):
        batch = Model.objects.all()[offset:offset + batch_size]
        for obj in batch:
            # Transform data
            obj.new_field = transform(obj.old_field)
            obj.save(update_fields=['new_field'])

def reverse_migration(apps, schema_editor):
    """
    Reverse the migration.
    """
    Model = apps.get_model('app', 'Model')
    Model.objects.all().update(new_field=None)

class Migration(migrations.Migration):
    operations = [
        migrations.RunPython(forward_migration, reverse_migration)
    ]
```

## Migration Testing Checklist

Before applying migration:

- ✅ Run on copy of production database
- ✅ Verify data integrity after migration
- ✅ Test rollback works
- ✅ Check migration duration (< 5 minutes ideal)
- ✅ Review SQL with `sqlmigrate`
- ✅ Confirm no downtime required
- ✅ Have rollback plan ready

## Validation Commands

Suggest running:

```bash
# Review SQL before applying
python manage.py sqlmigrate users 0002

# Check for issues
python manage.py makemigrations --check

# Dry run
python manage.py migrate --plan

# Apply to staging first
python manage.py migrate --database=staging
```

## Integration with CI/CD

```yaml
# .github/workflows/migrations.yml
- name: Check migrations safety
  run: |
    python manage.py makemigrations --check --dry-run --no-input
    python manage.py migrate --plan
```

## Success Criteria

✅ No data loss migrations
✅ All migrations tested on staging
✅ Rollback plan exists
✅ Large table migrations use CONCURRENT
✅ Data migrations properly batched
✅ Reversible migrations

## Behavior

**Proactive enforcement:**
- Analyze migrations without being asked
- Block unsafe migrations immediately
- Suggest safe 3-step patterns
- Generate data migration code
- Explain WHY the migration is risky

**Never:**
- Allow data loss migrations
- Let users skip safety checks
- Apply migrations without review
- Just warn without blocking unsafe operations

**When migration is unsafe:**
- Stop the process
- Explain the risk
- Provide safe alternative
- Generate corrected migration code
- User must acknowledge before proceeding

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.