# SM015: alter_unique_together

| Property       | Value                 |
| -------------- | --------------------- |
| **Rule ID**    | SM015                 |
| **Severity**   | WARNING               |
| **Name**       | alter_unique_together |
| **Applies to** | All databases         |

## What Triggers This Rule

This rule triggers when a migration uses `AlterUniqueTogether`:

```python
# Triggers SM015
migrations.AlterUniqueTogether(
    name='order',
    unique_together={('customer', 'product')},
)
```

## Why It's Flagged

`unique_together` is deprecated in favor of `UniqueConstraint`:

1. **Deprecated API** — Django recommends `UniqueConstraint` since Django 2.2
2. **Limited functionality** — `unique_together` can't use conditions, include fields, or set custom names
3. **Maintenance burden** — May be removed in future Django versions
4. **Same risks as SM009** — Still requires table scan and lock

### Django Documentation

From the [Django documentation](https://docs.djangoproject.com/en/stable/ref/models/options/#unique-together):

> Use `UniqueConstraint` with the `constraints` option instead.
> `unique_together` may be deprecated in the future.

## When It's Safe to Ignore

This warning can be safely ignored when:

1. **Maintaining legacy code** — Not worth refactoring existing constraints
2. **Quick prototype** — Will be replaced later
3. **Following existing patterns** — Codebase already uses `unique_together`

## How to Fix It

### Use UniqueConstraint Instead

Replace `unique_together` in your model:

```python
# ❌ Deprecated
class Order(models.Model):
    customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
    product = models.ForeignKey(Product, on_delete=models.CASCADE)

    class Meta:
        unique_together = [['customer', 'product']]

# ✅ Modern approach
class Order(models.Model):
    customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
    product = models.ForeignKey(Product, on_delete=models.CASCADE)

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=['customer', 'product'],
                name='unique_customer_product',
            ),
        ]
```

### Migration to Convert

```python
# Remove old unique_together
migrations.AlterUniqueTogether(
    name='order',
    unique_together=set(),
)

# Add new constraint
migrations.AddConstraint(
    model_name='order',
    constraint=models.UniqueConstraint(
        fields=['customer', 'product'],
        name='unique_customer_product',
    ),
)
```

### Benefits of UniqueConstraint

| Feature          | unique_together | UniqueConstraint    |
| ---------------- | --------------- | ------------------- |
| Custom name      | ❌              | ✅                  |
| Conditional      | ❌              | ✅ (`condition=`)   |
| Include fields   | ❌              | ✅ (`include=`)     |
| Deferrable       | ❌              | ✅ (`deferrable=`)  |
| Expression-based | ❌              | ✅ (`expressions=`) |

### Advanced Example

```python
class Meta:
    constraints = [
        # Basic unique constraint
        models.UniqueConstraint(
            fields=['customer', 'product'],
            name='unique_customer_product',
        ),
        # Conditional unique (only enforce for active orders)
        models.UniqueConstraint(
            fields=['customer', 'product'],
            condition=models.Q(status='active'),
            name='unique_active_order',
        ),
        # With included column for index-only scans
        models.UniqueConstraint(
            fields=['email'],
            include=['name'],
            name='unique_email_with_name',
        ),
    ]
```

## How to Suppress

### In Django Settings

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

## See Also

- [SM009: unique_constraint](SM009.md) — Related unique constraint rule
- [SM011: non_concurrent_unique](SM011.md) — PostgreSQL concurrent unique
- [Django UniqueConstraint documentation](https://docs.djangoproject.com/en/stable/ref/models/constraints/#uniqueconstraint)
