# SM009: unique_constraint

| Property       | Value             |
| -------------- | ----------------- |
| **Rule ID**    | SM009             |
| **Severity**   | ERROR             |
| **Name**       | unique_constraint |
| **Applies to** | All databases     |

## What Triggers This Rule

This rule triggers when a migration adds a unique constraint:

```python
# Triggers SM009
migrations.AddConstraint(
    model_name='user',
    constraint=models.UniqueConstraint(fields=['email'], name='unique_email'),
)
```

Also triggers for `unique=True` on a field:

```python
# Triggers SM009
migrations.AlterField(
    model_name='user',
    name='email',
    field=models.EmailField(unique=True),  # Adding unique=True
)
```

## Why It's Dangerous

Adding a unique constraint requires:

1. **Full table scan** — Check all rows for duplicates
2. **Table lock** — Prevents writes during the operation
3. **Index creation** — Builds a unique index (which also locks)
4. **Failure if duplicates exist** — Migration fails if data violates uniqueness

### Real-World Failure Scenario

```
Timeline (table with 10M rows):
├─ 0:00   Migration starts
├─ 0:00   Table locked for writes
├─ 0:02   Scanning for duplicates...
├─ 0:05   Building unique index...
├─ 0:10   Still building...
├─ 0:15   All INSERT/UPDATE queries timing out
├─ 0:20   Index complete
└─ 0:20   Application recovers
```

Or with duplicate data:

```
Timeline:
├─ 0:00  Migration starts
├─ 0:02  Scanning for duplicates...
├─ 0:03  ERROR: duplicate key value violates unique constraint
├─ 0:03  Detail: Key (email)=(john@example.com) appears twice
└─ 0:03  Migration failed
```

## When It's Safe to Ignore

This warning can be safely ignored when:

1. **The table is empty or small** — Quick operation
2. **Using maintenance mode** — No traffic during migration
3. **Data is verified unique** — Pre-checked for duplicates
4. **Using PostgreSQL with proper approach** — See fix below

## How to Fix It

### PostgreSQL: Create Index Concurrently First

The safe approach is to create a unique index concurrently, then add the constraint using that index:

**Step 1: Create unique index concurrently**

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

class Migration(migrations.Migration):
    atomic = False  # Required!

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

**Step 2: Add constraint using the index**

```python
migrations.RunSQL(
    sql='''
        ALTER TABLE myapp_user
        ADD CONSTRAINT unique_email
        UNIQUE USING INDEX user_email_unique_idx;
    ''',
    reverse_sql='ALTER TABLE myapp_user DROP CONSTRAINT unique_email;',
)
```

### Pre-Migration: Clean Duplicate Data

Before adding the constraint, remove duplicates:

```sql
-- Find duplicates
SELECT email, COUNT(*)
FROM users
GROUP BY email
HAVING COUNT(*) > 1;

-- Delete duplicates (keep first occurrence)
DELETE FROM users a
USING users b
WHERE a.id > b.id
  AND a.email = b.email;
```

### Other Databases

For MySQL/MariaDB, consider:

1. Adding the constraint during low-traffic periods
2. Using pt-online-schema-change (Percona Toolkit)
3. Accepting the brief downtime

## How to Suppress

### In Django Settings

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

## See Also

- [SM010: non_concurrent_index](SM010.md) — Related index creation rule
- [SM011: non_concurrent_unique](SM011.md) — PostgreSQL-specific unique constraint rule
- [Safe Patterns: Adding Unique Constraints](../patterns.md#adding-unique-constraints-postgresql)
