# SM005: add_foreign_key

| Property       | Value           |
| -------------- | --------------- |
| **Rule ID**    | SM005           |
| **Severity**   | WARNING         |
| **Name**       | add_foreign_key |
| **Applies to** | All databases   |

## What Triggers This Rule

This rule triggers when a migration adds a `ForeignKey` field:

```python
# Triggers SM005
migrations.AddField(
    model_name='order',
    name='customer',
    field=models.ForeignKey('customers.Customer', on_delete=models.CASCADE),
)
```

## Why It's Dangerous

When adding a foreign key constraint, the database must:

1. **Scan all existing rows** to validate the constraint
2. **Lock both tables** (the table being modified and the referenced table)
3. **Fail if any invalid references exist**

### Real-World Failure Scenario

```
Timeline (adding FK to table with 5M rows):
├─ 0:00   Migration starts
├─ 0:00   Tables locked
├─ 0:01   Validating all 5M rows...
├─ 0:05   Still validating...
├─ 0:10   Other queries timing out
├─ 0:15   Validation complete
└─ 0:15   Application recovers
```

Or if invalid data exists:

```
Timeline:
├─ 0:00  Migration starts
├─ 0:01  Validating rows...
├─ 0:02  ERROR: insert or update violates foreign key constraint
├─ 0:02  Row with customer_id=12345 references non-existent customer
└─ 0:02  Migration failed
```

## When It's Safe to Ignore

This warning can be safely ignored when:

1. **The table is empty or small** — Validation is quick
2. **You're certain all data is valid** — No orphaned references
3. **Using maintenance mode** — No traffic during migration
4. **The referenced table is small** — Lock duration is minimal

## How to Fix It

### PostgreSQL: Add Constraint NOT VALID

Add the foreign key without validating existing rows, then validate separately:

```python
# Migration 1: Add FK without validation
migrations.RunSQL(
    sql='''
        ALTER TABLE myapp_order
        ADD CONSTRAINT order_customer_fk
        FOREIGN KEY (customer_id)
        REFERENCES myapp_customer(id)
        NOT VALID;
    ''',
    reverse_sql='ALTER TABLE myapp_order DROP CONSTRAINT order_customer_fk;',
)
```

```python
# Migration 2: Validate (runs concurrently, doesn't block writes)
migrations.RunSQL(
    sql='ALTER TABLE myapp_order VALIDATE CONSTRAINT order_customer_fk;',
    reverse_sql=migrations.RunSQL.noop,
)
```

### Pre-Migration: Clean Invalid Data

Before adding the FK, ensure all references are valid:

```sql
-- Find orphaned references
SELECT o.id, o.customer_id
FROM orders o
LEFT JOIN customers c ON o.customer_id = c.id
WHERE o.customer_id IS NOT NULL
  AND c.id IS NULL;

-- Fix orphaned references
UPDATE orders
SET customer_id = NULL
WHERE customer_id NOT IN (SELECT id FROM customers);
```

### Alternative: Add Nullable FK First

```python
# Migration 1: Add nullable FK (won't validate NULL values)
migrations.AddField(
    model_name='order',
    name='customer',
    field=models.ForeignKey(
        'customers.Customer',
        on_delete=models.SET_NULL,
        null=True,  # Nullable first
    ),
)
```

```python
# Migration 2: After backfilling, make required
migrations.AlterField(
    model_name='order',
    name='customer',
    field=models.ForeignKey(
        'customers.Customer',
        on_delete=models.CASCADE,
        null=False,
    ),
)
```

## How to Suppress

### In Django Settings

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

## See Also

- [SM001: not_null_without_default](SM001.md) — Related constraint rule
- [Safe Patterns: Adding Foreign Keys](../patterns.md#adding-foreign-keys)
