# Django Model Design Decisions

## When to Use What

### Primary Keys

| Pattern | When to Use | Example |
|---------|-------------|---------|
| `uuid4` | Default for most models | `id = models.UUIDField(primary_key=True, default=uuid4)` |
| `uuid7` | Time-ordered data (findings, scans) | `id = models.UUIDField(primary_key=True, default=uuid7)` |

**Why uuid7 for time-series?** UUIDv7 includes timestamp, enabling efficient range queries and partitioning.

### Timestamps

| Field | Pattern | Purpose |
|-------|---------|---------|
| `inserted_at` | `auto_now_add=True, editable=False` | Creation time, never changes |
| `updated_at` | `auto_now=True, editable=False` | Last modification time |

### Soft Delete

```python
# Model
is_deleted = models.BooleanField(default=False)

# Custom manager (excludes deleted by default)
class ActiveProviderManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(is_deleted=False)

# Usage
objects = ActiveProviderManager()      # Normal queries
all_objects = models.Manager()         # Include deleted
```

### TextChoices Enums

```python
class StateChoices(models.TextChoices):
    AVAILABLE = "available", _("Available")
    SCHEDULED = "scheduled", _("Scheduled")
    EXECUTING = "executing", _("Executing")
    COMPLETED = "completed", _("Completed")
    FAILED = "failed", _("Failed")
```

### Constraints

| Constraint | When to Use |
|------------|-------------|
| `UniqueConstraint` | Prevent duplicates within tenant scope |
| `UniqueConstraint + condition` | Unique only for non-deleted records |
| `RowLevelSecurityConstraint` | ALL RLS-protected models (mandatory) |

```python
constraints = [
    # Unique provider UID per tenant (only for active providers)
    models.UniqueConstraint(
        fields=("tenant_id", "provider", "uid"),
        condition=Q(is_deleted=False),
        name="unique_provider_uids",
    ),
    # RLS constraint (REQUIRED for all tenant-scoped models)
    RowLevelSecurityConstraint(
        field="tenant_id",
        name="rls_on_%(class)s",
        statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
    ),
]
```

### Indexes

| Index Type | When to Use | Example |
|------------|-------------|---------|
| `models.Index` | Frequent queries | `fields=["tenant_id", "provider_id"]` |
| `GinIndex` | Full-text search, ArrayField | `fields=["text_search"]` |
| Conditional Index | Specific query patterns | `condition=Q(state="completed")` |
| Covering Index | Avoid table lookups | `include=["id", "name"]` |

```python
indexes = [
    # Common query pattern
    models.Index(
        fields=["tenant_id", "provider_id", "-inserted_at"],
        name="scans_prov_ins_desc_idx",
    ),
    # Conditional: only completed scans
    models.Index(
        fields=["tenant_id", "provider_id", "-inserted_at"],
        condition=Q(state=StateChoices.COMPLETED),
        name="scans_completed_idx",
    ),
    # Covering: include extra columns to avoid table lookup
    models.Index(
        fields=["tenant_id", "provider_id"],
        include=["id", "graph_database"],
        name="aps_active_graph_idx",
    ),
    # Full-text search
    GinIndex(fields=["text_search"], name="gin_resources_search_idx"),
]
```

### Full-Text Search

```python
from django.contrib.postgres.search import SearchVector, SearchVectorField

text_search = models.GeneratedField(
    expression=SearchVector("uid", weight="A", config="simple")
    + SearchVector("name", weight="B", config="simple"),
    output_field=SearchVectorField(),
    db_persist=True,
    null=True,
    editable=False,
)
```

### ArrayField

```python
from django.contrib.postgres.fields import ArrayField

groups = ArrayField(
    models.CharField(max_length=100),
    blank=True,
    null=True,
    help_text="Groups for categorization",
)
```

### JSONField

```python
# Structured data with defaults
metadata = models.JSONField(default=dict, blank=True)
scanner_args = models.JSONField(default=dict, blank=True)
```

### Encrypted Fields

```python
# Binary field for encrypted data
_secret = models.BinaryField(db_column="secret")

@property
def secret(self):
    # Decrypt on read
    decrypted_data = fernet.decrypt(self._secret)
    return json.loads(decrypted_data.decode())

@secret.setter
def secret(self, value):
    # Encrypt on write
    self._secret = fernet.encrypt(json.dumps(value).encode())
```

### Foreign Keys

| on_delete | When to Use |
|-----------|-------------|
| `CASCADE` | Child cannot exist without parent (Finding → Scan) |
| `SET_NULL` | Optional relationship, keep child (Task → PeriodicTask) |
| `PROTECT` | Prevent deletion if children exist |

```python
# Required relationship
provider = models.ForeignKey(
    Provider,
    on_delete=models.CASCADE,
    related_name="scans",
    related_query_name="scan",
)

# Optional relationship
scheduler_task = models.ForeignKey(
    PeriodicTask,
    on_delete=models.SET_NULL,
    null=True,
    blank=True,
)
```

### Many-to-Many with Through Table

```python
# On the model
tags = models.ManyToManyField(
    ResourceTag,
    through="ResourceTagMapping",
    related_name="resources",
)

# Through table (for RLS + extra fields)
class ResourceTagMapping(RowLevelSecurityProtectedModel):
    id = models.UUIDField(primary_key=True, default=uuid4)
    resource = models.ForeignKey(Resource, on_delete=models.CASCADE)
    tag = models.ForeignKey(ResourceTag, on_delete=models.CASCADE)

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=("tenant_id", "resource_id", "tag_id"),
                name="unique_resource_tag_mappings",
            ),
            RowLevelSecurityConstraint(...),
        ]
```

### Partitioned Tables

```python
from psqlextra.models import PostgresPartitionedModel
from psqlextra.types import PostgresPartitioningMethod

class Finding(PostgresPartitionedModel, RowLevelSecurityProtectedModel):
    class PartitioningMeta:
        method = PostgresPartitioningMethod.RANGE
        key = ["id"]  # UUIDv7 for time-based partitioning
```

**Use for:** High-volume, time-series data (findings, resource mappings)

### Model Validation

```python
def clean(self):
    super().clean()
    # Dynamic validation based on field value
    getattr(self, f"validate_{self.provider}_uid")(self.uid)

def save(self, *args, **kwargs):
    self.full_clean()  # Always validate before save
    super().save(*args, **kwargs)
```

### JSONAPIMeta

```python
class JSONAPIMeta:
    resource_name = "provider-groups"  # kebab-case, plural
```

---

## Decision Tree: New Model

```text
Is it tenant-scoped data?
├── Yes → Inherit RowLevelSecurityProtectedModel
│         Add RowLevelSecurityConstraint
│         Consider: soft-delete? partitioning?
└── No → Regular models.Model (rare in Prowler)

Does it need time-ordering for queries?
├── Yes → Use uuid7 for primary key
└── No → Use uuid4 (default)

Is it high-volume time-series data?
├── Yes → Use PostgresPartitionedModel
│         Partition by id (uuid7)
└── No → Regular model

Does it reference Provider?
├── Yes → Add ActiveProviderManager
│         Use CASCADE or filter is_deleted
└── No → Standard manager

Needs full-text search?
├── Yes → Add SearchVectorField + GinIndex
└── No → Skip
```
