# SM007: run_sql_no_reverse

| Property       | Value              |
| -------------- | ------------------ |
| **Rule ID**    | SM007              |
| **Severity**   | WARNING            |
| **Name**       | run_sql_no_reverse |
| **Applies to** | All databases      |

## What Triggers This Rule

This rule triggers when a migration contains `RunSQL` without `reverse_sql`:

```python
# Triggers SM007
migrations.RunSQL(
    sql='CREATE INDEX user_email_idx ON myapp_user(email);',
    # No reverse_sql provided
)
```

## Why It's Dangerous

Without `reverse_sql`, the migration cannot be rolled back:

1. **`migrate --reverse` fails** — Django can't undo the migration
2. **Stuck migrations** — Can't go back to previous state
3. **Testing difficulties** — Can't reset database state in tests
4. **Emergency rollback impossible** — If deployment fails, can't revert

### Real-World Failure Scenario

```
Timeline:
├─ 0:00  Deploy new version with migration
├─ 0:05  Bug discovered in new code
├─ 0:06  Attempt rollback: python manage.py migrate myapp 0005
├─ 0:06  ERROR: Migration myapp.0006 is not reversible
├─ 0:07  Manual database intervention required
├─ 0:10  DBA executes manual rollback SQL
└─ 0:15  Rollback complete (with stress and risk)
```

## When It's Safe to Ignore

This warning can be safely ignored when:

1. **The operation is truly irreversible** — Some operations can't be undone
2. **Using forward-only deployments** — Never roll back migrations
3. **The SQL is idempotent** — Running multiple times has no effect
4. **You have manual rollback procedures** — Documented outside Django

## How to Fix It

### Always Provide reverse_sql

```python
# ✅ Reversible
migrations.RunSQL(
    sql='CREATE INDEX user_email_idx ON myapp_user(email);',
    reverse_sql='DROP INDEX user_email_idx;',
)
```

### For Non-Reversible Operations

Use `migrations.RunSQL.noop` to explicitly mark as non-reversible:

```python
# Explicitly non-reversible (still triggers warning, but documents intent)
migrations.RunSQL(
    sql="ALTER TYPE status_enum ADD VALUE 'archived';",
    reverse_sql=migrations.RunSQL.noop,  # Cannot remove enum values
)
```

### Use state_operations for Schema Changes

When your SQL modifies schema, tell Django about it:

```python
migrations.RunSQL(
    sql='ALTER TABLE myapp_user ADD COLUMN temp_data JSONB;',
    reverse_sql='ALTER TABLE myapp_user DROP COLUMN temp_data;',
    state_operations=[
        migrations.AddField(
            model_name='user',
            name='temp_data',
            field=models.JSONField(null=True),
        ),
    ],
)
```

### Common Patterns

**Creating an index:**

```python
migrations.RunSQL(
    sql='CREATE INDEX CONCURRENTLY user_email_idx ON myapp_user(email);',
    reverse_sql='DROP INDEX CONCURRENTLY user_email_idx;',
)
```

**Adding a constraint:**

```python
migrations.RunSQL(
    sql='ALTER TABLE myapp_order ADD CONSTRAINT positive_amount CHECK (amount > 0);',
    reverse_sql='ALTER TABLE myapp_order DROP CONSTRAINT positive_amount;',
)
```

**Creating a view:**

```python
migrations.RunSQL(
    sql='''
        CREATE VIEW user_summary AS
        SELECT id, email, COUNT(orders.id) as order_count
        FROM users
        LEFT JOIN orders ON orders.user_id = users.id
        GROUP BY users.id;
    ''',
    reverse_sql='DROP VIEW user_summary;',
)
```

## How to Suppress

### In Django Settings

```python
SAFE_MIGRATIONS = {
    "DISABLED_RULES": ["SM007"],
}
```

## See Also

- [SM008: data_migration](SM008.md) — Related RunPython rule
- [SM012: enum_in_transaction](SM012.md) — PostgreSQL enum operations
- [SM016: run_python_no_reverse](SM016.md) — Similar rule for RunPython
- [Safe Patterns: RunSQL Best Practices](../patterns.md#runsql-best-practices)
