# Database Documentation ## Document Purpose This document describes the database schema, models, and data layer of the Aniworld application. --- ## 1. Database Overview ### Technology - **Database Engine**: SQLite 3 (default), PostgreSQL supported - **ORM**: SQLAlchemy 2.0 with async support (aiosqlite) - **Location**: `data/aniworld.db` (configurable via `DATABASE_URL`) Source: [src/config/settings.py](../src/config/settings.py#L53-L55) ### Connection Configuration ```python # Default connection string DATABASE_URL = "sqlite+aiosqlite:///./data/aniworld.db" # PostgreSQL alternative DATABASE_URL = "postgresql+asyncpg://user:pass@localhost/aniworld" ``` Source: [src/server/database/connection.py](../src/server/database/connection.py) --- ## 2. Entity Relationship Diagram ``` +-------------------+ +-------------------+ +------------------------+ | anime_series | | episodes | | download_queue_item | +-------------------+ +-------------------+ +------------------------+ | id (PK) |<--+ | id (PK) | +-->| id (PK, VARCHAR) | | key (UNIQUE) | | | series_id (FK)----+---+ | series_id (FK)---------+ | name | +---| | | status | | site | | season | | priority | | folder | | episode_number | | season | | created_at | | title | | episode | | updated_at | | file_path | | progress_percent | +-------------------+ | is_downloaded | | error_message | | created_at | | retry_count | | updated_at | | added_at | +-------------------+ | started_at | | completed_at | | created_at | | updated_at | +------------------------+ ``` --- ## 3. Table Schemas ### 3.1 anime_series Stores anime series metadata. | Column | Type | Constraints | Description | | ------------ | ------------- | -------------------------- | ------------------------------------------------------- | | `id` | INTEGER | PRIMARY KEY, AUTOINCREMENT | Internal database ID | | `key` | VARCHAR(255) | UNIQUE, NOT NULL, INDEX | **Primary identifier** - provider-assigned URL-safe key | | `name` | VARCHAR(500) | NOT NULL, INDEX | Display name of the series | | `site` | VARCHAR(500) | NOT NULL | Provider site URL | | `folder` | VARCHAR(1000) | NOT NULL | Filesystem folder name (metadata only) | | `created_at` | DATETIME | NOT NULL, DEFAULT NOW | Record creation timestamp | | `updated_at` | DATETIME | NOT NULL, ON UPDATE NOW | Last update timestamp | **Identifier Convention:** - `key` is the **primary identifier** for all operations (e.g., `"attack-on-titan"`) - `folder` is **metadata only** for filesystem operations (e.g., `"Attack on Titan (2013)"`) - `id` is used only for database relationships Source: [src/server/database/models.py](../src/server/database/models.py#L23-L87) ### 3.2 episodes Stores individual episode information. | Column | Type | Constraints | Description | | ---------------- | ------------- | ---------------------------- | ----------------------------- | | `id` | INTEGER | PRIMARY KEY, AUTOINCREMENT | Internal database ID | | `series_id` | INTEGER | FOREIGN KEY, NOT NULL, INDEX | Reference to anime_series.id | | `season` | INTEGER | NOT NULL | Season number (1-based) | | `episode_number` | INTEGER | NOT NULL | Episode number within season | | `title` | VARCHAR(500) | NULLABLE | Episode title if known | | `file_path` | VARCHAR(1000) | NULLABLE | Local file path if downloaded | | `is_downloaded` | BOOLEAN | NOT NULL, DEFAULT FALSE | Download status flag | | `created_at` | DATETIME | NOT NULL, DEFAULT NOW | Record creation timestamp | | `updated_at` | DATETIME | NOT NULL, ON UPDATE NOW | Last update timestamp | **Foreign Key:** - `series_id` -> `anime_series.id` (ON DELETE CASCADE) Source: [src/server/database/models.py](../src/server/database/models.py#L122-L181) ### 3.3 download_queue_item Stores download queue items with status tracking. | Column | Type | Constraints | Description | | ------------------ | ------------- | --------------------------- | ------------------------------ | | `id` | VARCHAR(36) | PRIMARY KEY | UUID identifier | | `series_id` | INTEGER | FOREIGN KEY, NOT NULL | Reference to anime_series.id | | `season` | INTEGER | NOT NULL | Season number | | `episode` | INTEGER | NOT NULL | Episode number | | `status` | VARCHAR(20) | NOT NULL, DEFAULT 'pending' | Download status | | `priority` | VARCHAR(10) | NOT NULL, DEFAULT 'NORMAL' | Queue priority | | `progress_percent` | FLOAT | NULLABLE | Download progress (0-100) | | `error_message` | TEXT | NULLABLE | Error description if failed | | `retry_count` | INTEGER | NOT NULL, DEFAULT 0 | Number of retry attempts | | `source_url` | VARCHAR(2000) | NULLABLE | Download source URL | | `added_at` | DATETIME | NOT NULL, DEFAULT NOW | When added to queue | | `started_at` | DATETIME | NULLABLE | When download started | | `completed_at` | DATETIME | NULLABLE | When download completed/failed | | `created_at` | DATETIME | NOT NULL, DEFAULT NOW | Record creation timestamp | | `updated_at` | DATETIME | NOT NULL, ON UPDATE NOW | Last update timestamp | **Status Values:** `pending`, `downloading`, `paused`, `completed`, `failed`, `cancelled` **Priority Values:** `LOW`, `NORMAL`, `HIGH` **Foreign Key:** - `series_id` -> `anime_series.id` (ON DELETE CASCADE) Source: [src/server/database/models.py](../src/server/database/models.py#L200-L300) --- ## 4. Indexes | Table | Index Name | Columns | Purpose | | --------------------- | ----------------------- | ----------- | --------------------------------- | | `anime_series` | `ix_anime_series_key` | `key` | Fast lookup by primary identifier | | `anime_series` | `ix_anime_series_name` | `name` | Search by name | | `episodes` | `ix_episodes_series_id` | `series_id` | Join with series | | `download_queue_item` | `ix_download_series_id` | `series_id` | Filter by series | | `download_queue_item` | `ix_download_status` | `status` | Filter by status | --- ## 5. Model Layer ### 5.1 SQLAlchemy ORM Models ```python # src/server/database/models.py class AnimeSeries(Base, TimestampMixin): __tablename__ = "anime_series" id: Mapped[int] = mapped_column(Integer, primary_key=True) key: Mapped[str] = mapped_column(String(255), unique=True, index=True) name: Mapped[str] = mapped_column(String(500), index=True) site: Mapped[str] = mapped_column(String(500)) folder: Mapped[str] = mapped_column(String(1000)) episodes: Mapped[List["Episode"]] = relationship( "Episode", back_populates="series", cascade="all, delete-orphan" ) ``` Source: [src/server/database/models.py](../src/server/database/models.py#L23-L87) ### 5.2 Pydantic API Models ```python # src/server/models/download.py class DownloadItem(BaseModel): id: str serie_id: str # Maps to anime_series.key serie_folder: str # Metadata only serie_name: str episode: EpisodeIdentifier status: DownloadStatus priority: DownloadPriority ``` Source: [src/server/models/download.py](../src/server/models/download.py#L63-L118) ### 5.3 Model Mapping | API Field | Database Column | Notes | | -------------- | --------------------- | ------------------ | | `serie_id` | `anime_series.key` | Primary identifier | | `serie_folder` | `anime_series.folder` | Metadata only | | `serie_name` | `anime_series.name` | Display name | --- ## 6. Repository Pattern The `QueueRepository` class provides data access abstraction. ```python class QueueRepository: async def save_item(self, item: DownloadItem) -> None: """Save or update a download item.""" async def get_all_items(self) -> List[DownloadItem]: """Get all items from database.""" async def delete_item(self, item_id: str) -> bool: """Delete item by ID.""" async def get_items_by_status( self, status: DownloadStatus ) -> List[DownloadItem]: """Get items filtered by status.""" ``` Source: [src/server/services/queue_repository.py](../src/server/services/queue_repository.py) --- ## 7. Database Service The `AnimeSeriesService` provides async CRUD operations. ```python class AnimeSeriesService: @staticmethod async def create( db: AsyncSession, key: str, name: str, site: str, folder: str ) -> AnimeSeries: """Create a new anime series.""" @staticmethod async def get_by_key( db: AsyncSession, key: str ) -> Optional[AnimeSeries]: """Get series by primary key identifier.""" ``` Source: [src/server/database/service.py](../src/server/database/service.py) --- ## 8. Data Integrity Rules ### Validation Constraints | Field | Rule | Error Message | | ------------------------- | ------------------------ | ------------------------------------- | | `anime_series.key` | Non-empty, max 255 chars | "Series key cannot be empty" | | `anime_series.name` | Non-empty, max 500 chars | "Series name cannot be empty" | | `episodes.season` | 0-1000 | "Season number must be non-negative" | | `episodes.episode_number` | 0-10000 | "Episode number must be non-negative" | Source: [src/server/database/models.py](../src/server/database/models.py#L89-L119) ### Cascade Rules - Deleting `anime_series` deletes all related `episodes` and `download_queue_item` --- ## 9. Migration Strategy Currently, SQLAlchemy's `create_all()` is used for schema creation. ```python # src/server/database/connection.py async def init_db(): async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) ``` For production migrations, Alembic is recommended but not yet implemented. Source: [src/server/database/connection.py](../src/server/database/connection.py) --- ## 10. Common Query Patterns ### Get all series with missing episodes ```python series = await db.execute( select(AnimeSeries).options(selectinload(AnimeSeries.episodes)) ) for serie in series.scalars(): downloaded = [e for e in serie.episodes if e.is_downloaded] ``` ### Get pending downloads ordered by priority ```python items = await db.execute( select(DownloadQueueItem) .where(DownloadQueueItem.status == "pending") .order_by( case( (DownloadQueueItem.priority == "HIGH", 1), (DownloadQueueItem.priority == "NORMAL", 2), (DownloadQueueItem.priority == "LOW", 3), ), DownloadQueueItem.added_at ) ) ``` --- ## 11. Database Location | Environment | Default Location | | ----------- | ------------------------------------------------- | | Development | `./data/aniworld.db` | | Production | Via `DATABASE_URL` environment variable | | Testing | In-memory SQLite (`sqlite+aiosqlite:///:memory:`) |