- Create transaction.py with @transactional decorator, atomic() context manager - Add TransactionPropagation modes: REQUIRED, REQUIRES_NEW, NESTED - Add savepoint support for nested transactions with partial rollback - Update connection.py with TransactionManager, get_transactional_session - Update service.py with bulk operations (bulk_mark_downloaded, bulk_delete) - Wrap QueueRepository.save_item() and clear_all() in atomic transactions - Add comprehensive tests (66 transaction tests, 90% coverage) - All 1090 tests passing
17 KiB
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 viaDATABASE_URL)
Source: src/config/settings.py
Connection Configuration
# 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
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:
keyis the primary identifier for all operations (e.g.,"attack-on-titan")folderis metadata only for filesystem operations (e.g.,"Attack on Titan (2013)")idis used only for database relationships
Source: src/server/database/models.py
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
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
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
# 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
5.2 Pydantic API Models
# 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
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. Transaction Support
6.1 Overview
The database layer provides comprehensive transaction support to ensure data consistency across compound operations. All write operations can be wrapped in explicit transactions.
Source: src/server/database/transaction.py
6.2 Transaction Utilities
| Component | Type | Description |
|---|---|---|
@transactional |
Decorator | Wraps function in transaction boundary |
atomic() |
Async context mgr | Provides atomic operation block |
atomic_sync() |
Sync context mgr | Sync version of atomic() |
TransactionContext |
Class | Explicit sync transaction control |
AsyncTransactionContext |
Class | Explicit async transaction control |
TransactionManager |
Class | Helper for manual transaction management |
6.3 Transaction Propagation Modes
| Mode | Behavior |
|---|---|
REQUIRED |
Use existing transaction or create new (default) |
REQUIRES_NEW |
Always create new transaction |
NESTED |
Create savepoint within existing transaction |
6.4 Usage Examples
Using @transactional decorator:
from src.server.database.transaction import transactional
@transactional()
async def compound_operation(db: AsyncSession, data: dict):
# All operations commit together or rollback on error
series = await AnimeSeriesService.create(db, ...)
episode = await EpisodeService.create(db, series_id=series.id, ...)
return series, episode
Using atomic() context manager:
from src.server.database.transaction import atomic
async def some_function(db: AsyncSession):
async with atomic(db) as tx:
await operation1(db)
await operation2(db)
# Auto-commits on success, rolls back on exception
Using savepoints for partial rollback:
async with atomic(db) as tx:
await outer_operation(db)
async with tx.savepoint() as sp:
await risky_operation(db)
if error_condition:
await sp.rollback() # Only rollback nested ops
await final_operation(db) # Still executes
Source: src/server/database/transaction.py
6.5 Connection Module Additions
| Function | Description |
|---|---|
get_transactional_session |
Session without auto-commit for transactions |
TransactionManager |
Helper class for manual transaction control |
is_session_in_transaction |
Check if session is in active transaction |
get_session_transaction_depth |
Get nesting depth of transactions |
Source: src/server/database/connection.py
7. Repository Pattern
The QueueRepository class provides data access abstraction.
class QueueRepository:
async def save_item(self, item: DownloadItem) -> None:
"""Save or update a download item (atomic operation)."""
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 clear_all(self) -> int:
"""Clear all items (atomic operation)."""
Note: Compound operations (save_item, clear_all) are wrapped in atomic() transactions.
Source: src/server/services/queue_repository.py
8. Database Service
The AnimeSeriesService provides async CRUD operations.
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."""
Bulk Operations
Services provide bulk operations for transaction-safe batch processing:
| Service | Method | Description |
|---|---|---|
EpisodeService |
bulk_mark_downloaded |
Mark multiple episodes at once |
DownloadQueueService |
bulk_delete |
Delete multiple queue items |
DownloadQueueService |
clear_all |
Clear entire queue |
UserSessionService |
rotate_session |
Revoke old + create new atomic |
UserSessionService |
cleanup_expired |
Bulk delete expired sessions |
Source: src/server/database/service.py
9. 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
Cascade Rules
- Deleting
anime_seriesdeletes all relatedepisodesanddownload_queue_item
10. Migration Strategy
Currently, SQLAlchemy's create_all() is used for schema creation.
# 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
11. Common Query Patterns
Get all series with missing episodes
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
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
)
)
12. Database Location
| Environment | Default Location |
|---|---|
| Development | ./data/aniworld.db |
| Production | Via DATABASE_URL environment variable |
| Testing | In-memory SQLite (sqlite+aiosqlite:///:memory:) |