# Models

Define your data structure using Python classes that map directly to PostgreSQL tables.

---

## What is a Model?

A **model** is a Python class that represents a table in your database. Each model:

- Defines what data you want to store (fields like title, email, price)
- Maps to a PostgreSQL table automatically
- Provides methods to create, read, update, and delete records
- Validates data before saving

Think of a model as a blueprint: it describes what a "Post" or "User" looks like, and Aksara handles all the database work.

```python
from aksara import Model, fields

class Article(Model):
    """A blog article stored in the 'articles' table."""
    
    title = fields.String(max_length=200)
    content = fields.Text()
    published = fields.Boolean(default=False)
    created_at = fields.DateTime(auto_now_add=True)
```

**What this creates**:
```
PostgreSQL table: articles
┌──────────────┬─────────────────┬─────────────┐
│ Column       │ Type            │ Constraints │
├──────────────┼─────────────────┼─────────────┤
│ id           │ UUID            │ PRIMARY KEY │
│ title        │ VARCHAR(200)    │ NOT NULL    │
│ content      │ TEXT            │ NOT NULL    │
│ published    │ BOOLEAN         │ DEFAULT false│
│ created_at   │ TIMESTAMPTZ     │ AUTO        │
└──────────────┴─────────────────┴─────────────┘
```

---

## Creating a Model

### Step 1: Import the Base Class and Fields

```python
from aksara import Model, fields
```

- `Model` — The base class all your models inherit from
- `fields` — Contains all field types (String, Integer, Boolean, etc.)

### Step 2: Define Your Class

```python
class Task(Model):
    """A task that users can complete."""
    
    title = fields.String(max_length=200)
    description = fields.Text(nullable=True)
    completed = fields.Boolean(default=False)
    due_date = fields.DateTime(nullable=True)
```

### Step 3: Create the Table

Run migrations to create the actual PostgreSQL table:

```bash
aksara makemigrations
aksara migrate
```

---

## What You Get Automatically

### Primary Key (`id`)

Every model automatically gets a UUID primary key. You don't need to define it:

```python
class User(Model):
    email = fields.Email(unique=True)
    # 'id' is automatically added as a UUID

user = await User.objects.create(email="alice@example.com")
print(user.id)  # UUID('550e8400-e29b-41d4-a716-446655440000')
```

**Why UUID instead of auto-increment?**

- Globally unique (safe for distributed systems)
- Can be generated client-side
- No sequential pattern (better security)
- PostgreSQL-native with `gen_random_uuid()`

### Table Name

The table name is derived from your class name:

| Model Name | Table Name |
|------------|------------|
| `Article` | `articles` |
| `User` | `users` |
| `Category` | `categories` |
| `UserProfile` | `user_profiles` |

**Override the table name** if needed:

```python
class Article(Model):
    __tablename__ = "blog_posts"  # Custom table name
    
    title = fields.String(max_length=200)
```

### Timestamps

Add automatic timestamps with `auto_now` and `auto_now_add`:

```python
class Article(Model):
    title = fields.String(max_length=200)
    
    # Set once when created, never changes
    created_at = fields.DateTime(auto_now_add=True)
    
    # Updated every time you save
    updated_at = fields.DateTime(auto_now=True)
```

---

## Working with Models

### Creating Records

```python
# Method 1: Create and save in one step
article = await Article.objects.create(
    title="Hello World",
    content="My first article!",
    published=True,
)

# Method 2: Create an instance, then save
article = Article(
    title="Hello World",
    content="My first article!",
)
await article.save()
```

### Reading Records

```python
# Get one record by ID
article = await Article.objects.get(id=article_id)

# Get one record by any field
article = await Article.objects.get(title="Hello World")

# Get all records
articles = await Article.objects.all()

# Filter records
published = await Article.objects.filter(published=True)
```

### Updating Records

```python
# Method 1: Modify and save
article = await Article.objects.get(id=article_id)
article.title = "New Title"
await article.save()

# Method 2: Update multiple at once
await Article.objects.filter(published=False).update(published=True)
```

### Deleting Records

```python
# Delete one record
article = await Article.objects.get(id=article_id)
await article.delete()

# Delete multiple records
await Article.objects.filter(published=False).delete()
```

---

## Field Types

Here's a quick reference of common fields:

### Text Fields

```python
class Example(Model):
    # Short text with max length
    name = fields.String(max_length=100)
    
    # Unlimited text
    bio = fields.Text()
    
    # Email with validation
    email = fields.Email(unique=True)
    
    # URL with validation
    website = fields.URL(nullable=True)
```

### Numeric Fields

```python
class Product(Model):
    # Whole numbers
    quantity = fields.Integer(default=0)
    
    # Decimal numbers (for money)
    price = fields.Decimal(max_digits=10, decimal_places=2)
    
    # Floating point (for calculations)
    rating = fields.Float(nullable=True)
```

### Date and Time

```python
class Event(Model):
    # Date only (no time)
    date = fields.Date()
    
    # Time only (no date)
    start_time = fields.Time()
    
    # Full timestamp with timezone
    created_at = fields.DateTime(auto_now_add=True)
```

### Other Types

```python
class Settings(Model):
    # True/False
    is_active = fields.Boolean(default=True)
    
    # JSON data (dictionaries, lists)
    preferences = fields.JSON(default=dict)
    
    # PostgreSQL arrays
    tags = fields.Array(base_type="text", default=list)
    
    # UUID
    external_id = fields.UUID(nullable=True)
```

See [Fields Reference](fields.md) for complete documentation.

---

## Field Options

All fields accept these common options:

| Option | What It Does | Example |
|--------|--------------|---------|
| `nullable` | Allow NULL values | `fields.String(nullable=True)` |
| `default` | Default value | `fields.Boolean(default=False)` |
| `unique` | Ensure uniqueness | `fields.Email(unique=True)` |
| `db_index` | Create database index | `fields.String(db_index=True)` |
| `primary_key` | Mark as primary key | `fields.UUID(primary_key=True)` |

### Examples

```python
class User(Model):
    # Required, must be unique
    email = fields.Email(unique=True)
    
    # Optional (can be empty)
    phone = fields.String(max_length=20, nullable=True)
    
    # Has a default value
    is_active = fields.Boolean(default=True)
    
    # Indexed for fast lookups
    username = fields.String(max_length=50, unique=True, db_index=True)
```

---

## Model Meta Options

Configure model-wide settings using the `Meta` class:

```python
class Article(Model):
    title = fields.String(max_length=200)
    created_at = fields.DateTime(auto_now_add=True)
    
    class Meta:
        # Custom table name
        table_name = "blog_articles"
        
        # Default ordering (newest first)
        ordering = ["-created_at"]
        
        # Database indexes for performance
        indexes = [
            ("title",),  # Single column index
            ("created_at", "title"),  # Composite index
        ]
        
        # Unique together constraints
        unique_together = [
            ("author_id", "slug"),  # Same author can't have duplicate slugs
        ]
        
        # App label for admin grouping
        app_label = "blog"
```

### Meta Options Reference

| Option | What It Does | Example |
|--------|--------------|---------|
| `table_name` | Custom database table name | `"blog_posts"` |
| `ordering` | Default sort order | `["-created_at"]` |
| `indexes` | Database indexes | `[("field1", "field2")]` |
| `unique_together` | Multi-column uniqueness | `[("user_id", "slug")]` |
| `app_label` | Group in admin | `"blog"` |

---

## AI Metadata

Aksara models can include metadata that helps AI agents understand your data:

```python
class Product(Model):
    name = fields.String(
        max_length=200,
        ai_description="The product's display name shown to customers",
    )
    
    price = fields.Decimal(
        max_digits=10,
        decimal_places=2,
        ai_description="Price in USD, excluding tax",
    )
    
    internal_cost = fields.Decimal(
        max_digits=10,
        decimal_places=2,
        ai_sensitive=True,  # Hide from AI context
    )
    
    class Meta:
        ai_description = "Products available for purchase in the store"
```

| Option | What It Does |
|--------|--------------|
| `ai_description` | Human-readable description for AI |
| `ai_sensitive` | Hide field from AI context |
| `ai_agent_writable` | Allow/prevent AI from modifying |

---

## Relationships

Models can reference other models:

```python
from aksara import Model, fields, CASCADE

class Author(Model):
    name = fields.String(max_length=100)

class Post(Model):
    title = fields.String(max_length=200)
    
    # Many posts can have one author
    author = fields.ForeignKey(Author, on_delete=CASCADE, related_name="posts")
```

See [Relations](relations.md) for complete documentation.

---

## Complete Example

```python
from aksara import Model, fields, CASCADE
from decimal import Decimal

class Category(Model):
    """Product categories like Electronics, Clothing, etc."""
    
    name = fields.String(max_length=100, unique=True)
    slug = fields.String(max_length=100, unique=True)
    description = fields.Text(nullable=True)
    
    class Meta:
        ordering = ["name"]
        app_label = "store"


class Product(Model):
    """Items available for purchase."""
    
    # Basic info
    name = fields.String(max_length=200)
    slug = fields.String(max_length=200, unique=True)
    description = fields.Text()
    
    # Pricing
    price = fields.Decimal(max_digits=10, decimal_places=2)
    sale_price = fields.Decimal(max_digits=10, decimal_places=2, nullable=True)
    
    # Inventory
    sku = fields.String(max_length=50, unique=True)
    stock_quantity = fields.Integer(default=0)
    
    # Status
    is_active = fields.Boolean(default=True)
    is_featured = fields.Boolean(default=False)
    
    # Relationships
    category = fields.ForeignKey(
        Category,
        on_delete=CASCADE,
        related_name="products",
    )
    
    # Metadata
    tags = fields.Array(base_type="text", default=list)
    attributes = fields.JSON(default=dict)  # e.g., {"color": "red", "size": "M"}
    
    # Timestamps
    created_at = fields.DateTime(auto_now_add=True)
    updated_at = fields.DateTime(auto_now=True)
    
    class Meta:
        ordering = ["-created_at"]
        app_label = "store"
        indexes = [
            ("category_id", "is_active"),
            ("sku",),
        ]


# Using the models
async def example():
    # Create a category
    electronics = await Category.objects.create(
        name="Electronics",
        slug="electronics",
        description="Gadgets and devices",
    )
    
    # Create a product
    phone = await Product.objects.create(
        name="Smartphone X",
        slug="smartphone-x",
        description="Latest smartphone with amazing features",
        price=Decimal("999.99"),
        sku="PHONE-001",
        stock_quantity=100,
        category=electronics,
        tags=["smartphone", "mobile", "5g"],
        attributes={"color": "black", "storage": "256GB"},
    )
    
    # Query products
    active_products = await Product.objects.filter(
        is_active=True,
        stock_quantity__gt=0,
    ).order_by("-created_at")
    
    # Access relationship
    for product in active_products:
        category = await product.category
        print(f"{product.name} in {category.name}")
```

---

## Related Documentation

- [Fields](fields.md) — All field types and options
- [Querying](querying.md) — Filter, sort, and retrieve data
- [Relations](relations.md) — ForeignKey, ManyToMany, OneToOne
- [Migrations](migrations.md) — Create and update tables
