# SM011: non_concurrent_unique

| Property       | Value                 |
| -------------- | --------------------- |
| **Rule ID**    | SM011                 |
| **Severity**   | ERROR                 |
| **Name**       | non_concurrent_unique |
| **Applies to** | PostgreSQL only       |

## What Triggers This Rule

This rule triggers when a migration adds a unique constraint without first creating a concurrent unique index:

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

## Why It's Dangerous

Adding a unique constraint directly causes PostgreSQL to:

1. **Lock the table** for the duration
2. **Scan all rows** for duplicates
3. **Build a unique index** while holding the lock

This combines the worst of both SM009 (unique constraint) and SM010 (index creation).

### Real-World Failure Scenario

```
Timeline (5M row table):
├─ 0:00   Migration starts
├─ 0:00   ACCESS EXCLUSIVE lock acquired
├─ 0:01   Scanning for duplicates...
├─ 0:03   Building unique index...
├─ 0:05   All writes blocked
├─ 0:10   Still building index...
├─ 0:15   Connection pool exhausted
├─ 0:20   Index complete
└─ 0:20   Backlog of queries executes
```

## When It's Safe to Ignore

This warning can be safely ignored when:

1. **The table is empty or very small** — Instant operation
2. **Using maintenance mode** — No traffic during migration
3. **Not using PostgreSQL** — Rule only applies to PostgreSQL

## How to Fix It

### Two-Step Approach: Index First, Then Constraint

**Step 1: Create unique index concurrently**

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

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

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

**Step 2: Convert index to constraint**

```python
class Migration(migrations.Migration):
    operations = [
        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;',
        ),
    ]
```

### Understanding the PostgreSQL Syntax

The `UNIQUE USING INDEX` clause tells PostgreSQL to use an existing index as the backing index for the constraint, avoiding the need to build a new one.

Requirements:

- The index must already exist
- The index must be unique
- The index must cover the same columns

### Alternative: Just Use the Index

If you only need uniqueness enforcement (not the formal constraint), you can skip step 2. A unique index provides the same guarantee:

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

class Migration(migrations.Migration):
    atomic = False

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

!!! note
    Django's `unique=True` on a field expects a constraint, not just an index. If you're using model-level uniqueness, you'll need the constraint.

## How to Suppress

### In Django Settings

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

## See Also

- [SM009: unique_constraint](SM009.md) — General unique constraint rule
- [SM010: non_concurrent_index](SM010.md) — Index creation rule
- [Safe Patterns: Adding Unique Constraints](../patterns.md#adding-unique-constraints-postgresql)
