# 🍃 ModelAPI: Lightweight SQLite ORM

A simple, model-centric ORM for SQLite that feels familiar if you've used Laravel's Eloquent. Designed for fast prototyping and projects that value readability and minimalism.

> **Heads up:** `ModelAPI` is **not** the same as `TableAPI`. While both serve as ORMs, `ModelAPI` focuses on declarative, class-based models — ideal for those who prefer OOP-style code.

---

## 📘 Table of Contents

- [🍃 ModelAPI: Lightweight SQLite ORM](#-modelapi-lightweight-sqlite-orm)
  - [📘 Table of Contents](#-table-of-contents)
  - [🚀 Getting Started](#-getting-started)
    - [🧩 Installation](#-installation)
    - [🛠 Database Setup](#-database-setup)
  - [🧱 Defining Models](#-defining-models)
    - [🏷 `__schema__` and Field Declarations](#-__schema__-and-field-declarations)
    - [🔁 Auto-Generating IDs](#-auto-generating-ids)
  - [🛠 CRUD Operations](#-crud-operations)
    - [✅ Create](#-create)
    - [🔍 Read](#-read)
      - [`all()`, `first()`, and `one()`](#all-first-and-one)
      - [Flexible Queries with `where()`](#flexible-queries-with-where)
    - [📝 Update](#-update)
    - [🗑 Delete](#-delete)
  - [💻 CLI Example](#-cli-example)
  - [🧠 Best Practices](#-best-practices)
  - [⚠️ Common Pitfalls](#️-common-pitfalls)

---

## 🚀 Getting Started

### 🧩 Installation

You’ll need the core dependency, `luminadb`:

```bash
pip install luminadb
```

If you're developing locally and already have it, just ensure it's accessible in your Python path.

---

### 🛠 Database Setup

Start by initializing your database:

```python
# db.py
from luminadb import Database

db = Database("notes.db")  # Or use ":memory:" for an in-memory DB
```

You’ll reuse `db` across all your models.

---

## 🧱 Defining Models

Models define how your data is structured. Think of each model as a table blueprint.

```python
# model/notes.py
from luminadb import model, BaseModel, Primary
from db import db

@model(db)
class Notes(BaseModel):
    __schema__ = (Primary('id'),)

    id: str
    title: str
    content: str
```

### 🏷 `__schema__` and Field Declarations

Define your fields using schema helpers like:

- `Primary(field_name)`
- `Unique(field_name)`
- `Foreign(field_name, f"{ref_table}/{ref_column}")`

These help ensure integrity and enforce constraints under the hood.

---

### 🔁 Auto-Generating IDs

If your primary key is a UUID or something dynamic, define `__auto_id__`:

```python
from uuid import uuid4

@model(db)
class Notes(BaseModel):
    __schema__ = (Primary("id"),)
    __auto_id__ = lambda: str(uuid4())

    id: str
    title: str
    content: str
```

Whenever you call `.create()` without an `id`, this auto-generator kicks in.

---

## 🛠 CRUD Operations

### ✅ Create

Create a new record like this:

```python
Notes.create(title="Meeting", content="Discuss roadmap")
```

Interactive input? No problem:

```python
title = input("Title: ")
content = input("Content: ")
Notes.create(title=title, content=content)
```

---

### 🔍 Read

#### `all()`, `first()`, and `one()`

```python
Notes.all()                 # Returns a list of all notes
Notes.first(id="abc")       # First match or None
Notes.one(id="abc")         # Exactly one match; raises error if 0 or >1
```

#### Flexible Queries with `where()`

Chainable query builder:

```python
Notes.where(title="Roadmap").fetch_one()
Notes.where().limit(5).fetch()
Notes.where().offset(5).fetch()
Notes.where().count()
```

---

### 📝 Update

First, fetch a record — then update it:

```python
note = Notes.first(id="abc")
if note:
    note.update(title="Updated title", content="Updated body")
```

---

### 🗑 Delete

```python
note = Notes.first(id="abc")
if note:
    note.delete()
```

This permanently removes the record from the database.

---

## 💻 CLI Example

Here’s a complete interactive CLI app:

```python
# cli.py
from model.notes import Notes
from enum import IntEnum

class CMD(IntEnum):
    DISPLAY = 1
    CREATE = 2
    UPDATE = 3
    DELETE = 4
    EXIT = 5

def display():
    print("---")
    for note in Notes.all():
        print(f"ID: {note.id}\nTitle: {note.title}\nContent: {note.content}\n---")

def create():
    Notes.create(title=input("Title: "), content=input("Content: "))

def update():
    note = Notes.first(id=input("ID: "))
    if note:
        note.update(title=input("Title: "), content=input("Content: "))
    else:
        print("Note not found.")

def delete():
    note = Notes.first(id=input("ID: "))
    if note:
        note.delete()
    else:
        print("Note not found.")

def main():
    while True:
        print("1. Display\n2. Create\n3. Update\n4. Delete\n5. Exit")
        try:
            cmd = int(input("Select: "))
            if cmd == CMD.DISPLAY: display()
            elif cmd == CMD.CREATE: create()
            elif cmd == CMD.UPDATE: update()
            elif cmd == CMD.DELETE: delete()
            elif cmd == CMD.EXIT: break
        except Exception as e:
            print(f"{type(e).__name__}: {e}")

if __name__ == '__main__':
    main()
```

---

## 🧠 Best Practices

✅ **Define all constraints in `__schema__`** — Primary, Foreign, and Unique.
✅ **Use `__auto_id__`** for consistent ID generation (especially UUIDs).
✅ **Keep models minimal** — push business logic elsewhere (CLI, service layer, etc).
✅ Use `.where().count()` instead of loading all records just to count them.
✅ Only use `.one()` when you’re 100% sure the result is unique.

---

## ⚠️ Common Pitfalls

❌ Missing `@model(db)` — your model won’t be registered.
❌ Using `.one()` on multi-match queries — it will throw an exception.
❌ Forgetting `.fetch()` or `.fetch_one()` after `.where()` — it won’t run.
❌ Assuming `.create()` returns an object — it returns `None`.
