# SM017: check_constraint

| Property       | Value            |
| -------------- | ---------------- |
| **Rule ID**    | SM017            |
| **Severity**   | WARNING          |
| **Name**       | check_constraint |
| **Applies to** | All databases    |

## What Triggers This Rule

This rule triggers when a migration adds a CHECK constraint:

```python
# Triggers SM017
migrations.AddConstraint(
    model_name='order',
    constraint=models.CheckConstraint(
        check=models.Q(amount__gte=0),  # or condition= for Django 5.1+
        name='positive_amount',
    ),
)
```

## Why It's Dangerous

When adding a CHECK constraint, the database must:

1. **Validate all existing rows** against the constraint
2. **Lock the table** during validation
3. **Fail if any rows violate** the constraint

### Real-World Failure Scenario

```
Timeline (table with 5M rows):
├─ 0:00   Migration starts
├─ 0:00   Table locked
├─ 0:01   Validating all rows against CHECK (amount >= 0)...
├─ 0:05   Still validating...
├─ 0:07   ERROR: check constraint "positive_amount" is violated by some row
├─ 0:07   Migration failed
└─ 0:07   Or if data is valid: 10 minutes of downtime
```

## 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 violations exist
3. **Using maintenance mode** — No traffic during migration
4. **Using PostgreSQL NOT VALID** — See fix below

## How to Fix It

### PostgreSQL: Add NOT VALID Then Validate

Add the constraint without validating, then validate separately:

**Step 1: Add constraint NOT VALID**

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

**Step 2: Validate separately**

The validation step acquires a weaker lock that allows reads and writes:

```python
migrations.RunSQL(
    sql='ALTER TABLE myapp_order VALIDATE CONSTRAINT positive_amount;',
    reverse_sql=migrations.RunSQL.noop,
)
```

### Pre-Migration: Clean Invalid Data

Before adding the constraint, fix any violations:

```sql
-- Find violations
SELECT id, amount
FROM orders
WHERE amount < 0;

-- Fix violations
UPDATE orders
SET amount = 0
WHERE amount < 0;
```

Or in a migration:

```python
def fix_negative_amounts(apps, schema_editor):
    Order = apps.get_model('myapp', 'Order')
    Order.objects.filter(amount__lt=0).update(amount=0)

migrations.RunPython(fix_negative_amounts, migrations.RunPython.noop)
```

### Django 5.1+ Syntax Note

In Django 5.1+, `CheckConstraint` uses `condition` instead of `check`:

```python
# Django 5.1+
models.CheckConstraint(
    condition=models.Q(amount__gte=0),  # Use 'condition'
    name='positive_amount',
)

# Django < 5.1
models.CheckConstraint(
    check=models.Q(amount__gte=0),  # Use 'check'
    name='positive_amount',
)
```

### Other Databases

For MySQL/MariaDB:

- CHECK constraints are validated immediately
- Consider adding during low-traffic periods
- Pre-validate data before adding the constraint

## Common CHECK Constraints

**Positive values:**

```python
models.CheckConstraint(
    condition=models.Q(amount__gte=0),
    name='positive_amount',
)
```

**Date range:**

```python
models.CheckConstraint(
    condition=models.Q(end_date__gte=models.F('start_date')),
    name='valid_date_range',
)
```

**Enum-like values:**

```python
models.CheckConstraint(
    condition=models.Q(status__in=['pending', 'active', 'completed']),
    name='valid_status',
)
```

**Percentage range:**

```python
models.CheckConstraint(
    condition=models.Q(percentage__gte=0, percentage__lte=100),
    name='valid_percentage',
)
```

## How to Suppress

### In Django Settings

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

## See Also

- [SM009: unique_constraint](SM009.md) — Related constraint rule
- [SM005: add_foreign_key](SM005.md) — FK constraint validation
- [Safe Patterns: Adding CHECK Constraints](../patterns.md#adding-check-constraints)
