# SM010: non_concurrent_index

| Property       | Value                |
| -------------- | -------------------- |
| **Rule ID**    | SM010                |
| **Severity**   | ERROR                |
| **Name**       | non_concurrent_index |
| **Applies to** | PostgreSQL only      |

## What Triggers This Rule

This rule triggers when a migration creates an index without using `CONCURRENTLY`:

```python
# Triggers SM010 (on PostgreSQL)
migrations.AddIndex(
    model_name='user',
    index=models.Index(fields=['email'], name='user_email_idx'),
)
```

## Why It's Dangerous

Standard `CREATE INDEX` in PostgreSQL:

1. **Acquires a SHARE lock** on the table
2. **Blocks all INSERT, UPDATE, DELETE** operations
3. **Holds the lock** for the entire index build duration

On large tables, this can mean minutes or hours of downtime.

### Real-World Failure Scenario

```
Timeline (10M row table):
├─ 0:00   Migration: CREATE INDEX user_email_idx ON users(email)
├─ 0:00   SHARE lock acquired
├─ 0:01   Building index...
├─ 0:02   User tries to update profile → blocked
├─ 0:03   New user registration → blocked
├─ 0:04   Background job update → blocked
├─ 0:05   Connection pool exhausted
├─ 0:10   Index build completes
└─ 0:10   Blocked queries execute (or timeout)
```

## When It's Safe to Ignore

This warning can be safely ignored when:

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

## How to Fix It

### Use AddIndexConcurrently

Django provides `AddIndexConcurrently` for PostgreSQL:

```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_idx'),
        ),
    ]
```

!!! warning "Important"
    You **must** set `atomic = False` on the migration class. `CREATE INDEX CONCURRENTLY` cannot run inside a transaction.

### Using RunSQL

For more control:

```python
class Migration(migrations.Migration):
    atomic = False

    operations = [
        migrations.RunSQL(
            sql='CREATE INDEX CONCURRENTLY user_email_idx ON myapp_user(email);',
            reverse_sql='DROP INDEX CONCURRENTLY user_email_idx;',
        ),
    ]
```

### For Removing Indexes

Use `RemoveIndexConcurrently`:

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

class Migration(migrations.Migration):
    atomic = False

    operations = [
        RemoveIndexConcurrently(
            model_name='user',
            name='user_email_idx',
        ),
    ]
```

### Handling Index Creation Failures

`CREATE INDEX CONCURRENTLY` can fail without leaving the table locked, but it may leave an invalid index. Check for invalid indexes:

```sql
SELECT indexname, indexdef
FROM pg_indexes
WHERE schemaname = 'public'
  AND indexname IN (
    SELECT indexrelid::regclass::text
    FROM pg_index
    WHERE NOT indisvalid
  );
```

If an invalid index exists, drop it and retry:

```sql
DROP INDEX CONCURRENTLY user_email_idx;
-- Then run the migration again
```

## How to Suppress

### In Django Settings

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

## See Also

- [SM009: unique_constraint](SM009.md) — Related constraint rule
- [SM011: non_concurrent_unique](SM011.md) — Unique constraint concurrent rule
- [Safe Patterns: Creating Indexes](../patterns.md#creating-indexes-postgresql)
- [PostgreSQL Documentation: CREATE INDEX](https://www.postgresql.org/docs/current/sql-createindex.html#SQL-CREATEINDEX-CONCURRENTLY)
