feat: Implement SQLAlchemy database layer with comprehensive models

Implemented a complete database layer for persistent storage of anime series,
episodes, download queue, and user sessions using SQLAlchemy ORM.

Features:
- 4 SQLAlchemy models: AnimeSeries, Episode, DownloadQueueItem, UserSession
- Automatic timestamp tracking via TimestampMixin
- Foreign key relationships with cascade deletes
- Async and sync database session support
- FastAPI dependency injection integration
- SQLite optimizations (WAL mode, foreign keys)
- Enum types for status and priority fields

Models:
- AnimeSeries: Series metadata with one-to-many relationships
- Episode: Individual episodes linked to series
- DownloadQueueItem: Queue persistence with progress tracking
- UserSession: JWT session storage with expiry and revocation

Database Management:
- Async engine creation with aiosqlite
- Session factory with proper lifecycle
- Connection pooling configuration
- Automatic table creation on initialization

Testing:
- 19 comprehensive unit tests (all passing)
- In-memory SQLite for test isolation
- Relationship and constraint validation
- Query operation testing

Documentation:
- Comprehensive database section in infrastructure.md
- Database package README with examples
- Implementation summary document
- Usage guides and troubleshooting

Dependencies:
- Added: sqlalchemy>=2.0.35 (Python 3.13 compatible)
- Added: alembic==1.13.0 (for future migrations)
- Added: aiosqlite>=0.19.0 (async SQLite driver)

Files:
- src/server/database/__init__.py (package exports)
- src/server/database/base.py (base classes and mixins)
- src/server/database/models.py (ORM models, ~435 lines)
- src/server/database/connection.py (connection management)
- src/server/database/migrations.py (migration placeholder)
- src/server/database/README.md (package documentation)
- tests/unit/test_database_models.py (19 test cases)
- DATABASE_IMPLEMENTATION_SUMMARY.md (implementation summary)

Closes #9 Database Layer implementation task
This commit is contained in:
2025-10-17 20:46:21 +02:00
parent 0d6cade56c
commit ff0d865b7c
12 changed files with 2390 additions and 49 deletions

View File

@@ -0,0 +1,293 @@
# Database Layer
SQLAlchemy-based database layer for the Aniworld web application.
## Overview
This package provides persistent storage for anime series, episodes, download queue, and user sessions using SQLAlchemy ORM.
## Quick Start
### Installation
Install required dependencies:
```bash
pip install sqlalchemy alembic aiosqlite
```
Or use the project requirements:
```bash
pip install -r requirements.txt
```
### Initialization
Initialize the database on application startup:
```python
from src.server.database import init_db, close_db
# Startup
await init_db()
# Shutdown
await close_db()
```
### Usage in FastAPI
Use the database session dependency in your endpoints:
```python
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
from src.server.database import get_db_session, AnimeSeries
from sqlalchemy import select
@app.get("/anime")
async def get_anime(db: AsyncSession = Depends(get_db_session)):
result = await db.execute(select(AnimeSeries))
return result.scalars().all()
```
## Models
### AnimeSeries
Represents an anime series with metadata and relationships.
```python
series = AnimeSeries(
key="attack-on-titan",
name="Attack on Titan",
site="https://aniworld.to",
folder="/anime/attack-on-titan",
description="Epic anime about titans",
status="completed",
total_episodes=75
)
```
### Episode
Individual episodes linked to series.
```python
episode = Episode(
series_id=series.id,
season=1,
episode_number=5,
title="The Fifth Episode",
is_downloaded=True
)
```
### DownloadQueueItem
Download queue with progress tracking.
```python
from src.server.database.models import DownloadStatus, DownloadPriority
item = DownloadQueueItem(
series_id=series.id,
season=1,
episode_number=3,
status=DownloadStatus.DOWNLOADING,
priority=DownloadPriority.HIGH,
progress_percent=45.5
)
```
### UserSession
User authentication sessions.
```python
from datetime import datetime, timedelta
session = UserSession(
session_id="unique-session-id",
token_hash="hashed-jwt-token",
expires_at=datetime.utcnow() + timedelta(hours=24),
is_active=True
)
```
## Mixins
### TimestampMixin
Adds automatic timestamp tracking:
```python
from src.server.database.base import Base, TimestampMixin
class MyModel(Base, TimestampMixin):
__tablename__ = "my_table"
# created_at and updated_at automatically added
```
### SoftDeleteMixin
Provides soft delete functionality:
```python
from src.server.database.base import Base, SoftDeleteMixin
class MyModel(Base, SoftDeleteMixin):
__tablename__ = "my_table"
# Usage
instance.soft_delete() # Mark as deleted
instance.is_deleted # Check if deleted
instance.restore() # Restore deleted record
```
## Configuration
Configure database via environment variables:
```bash
DATABASE_URL=sqlite:///./data/aniworld.db
LOG_LEVEL=DEBUG # Enables SQL query logging
```
Or in code:
```python
from src.config.settings import settings
settings.database_url = "sqlite:///./data/aniworld.db"
```
## Migrations (Future)
Alembic is installed for database migrations:
```bash
# Initialize Alembic
alembic init alembic
# Generate migration
alembic revision --autogenerate -m "Description"
# Apply migrations
alembic upgrade head
# Rollback
alembic downgrade -1
```
## Testing
Run database tests:
```bash
pytest tests/unit/test_database_models.py -v
```
The test suite uses an in-memory SQLite database for isolation and speed.
## Architecture
- **base.py**: Base declarative class and mixins
- **models.py**: SQLAlchemy ORM models (4 models)
- **connection.py**: Engine, session factory, dependency injection
- **migrations.py**: Alembic migration placeholder
- ****init**.py**: Package exports
## Database Schema
```
anime_series (id, key, name, site, folder, ...)
├── episodes (id, series_id, season, episode_number, ...)
└── download_queue (id, series_id, season, episode_number, status, ...)
user_sessions (id, session_id, token_hash, expires_at, ...)
```
## Production Considerations
### SQLite (Current)
- Single file: `data/aniworld.db`
- WAL mode for concurrency
- Foreign keys enabled
- Static connection pool
### PostgreSQL/MySQL (Future)
For multi-process deployments:
```python
DATABASE_URL=postgresql+asyncpg://user:pass@host/db
# or
DATABASE_URL=mysql+aiomysql://user:pass@host/db
```
Configure connection pooling:
```python
engine = create_async_engine(
url,
pool_size=10,
max_overflow=20,
pool_pre_ping=True
)
```
## Performance Tips
1. **Indexes**: Models have indexes on frequently queried columns
2. **Relationships**: Use `selectinload()` or `joinedload()` for eager loading
3. **Batching**: Use bulk operations for multiple inserts/updates
4. **Query Optimization**: Profile slow queries in DEBUG mode
Example with eager loading:
```python
from sqlalchemy.orm import selectinload
result = await db.execute(
select(AnimeSeries)
.options(selectinload(AnimeSeries.episodes))
.where(AnimeSeries.key == "attack-on-titan")
)
series = result.scalar_one()
# episodes already loaded, no additional queries
```
## Troubleshooting
### Database not initialized
```
RuntimeError: Database not initialized. Call init_db() first.
```
Solution: Call `await init_db()` during application startup.
### Table does not exist
```
sqlalchemy.exc.OperationalError: no such table: anime_series
```
Solution: `Base.metadata.create_all()` is called automatically by `init_db()`.
### Foreign key constraint failed
```
sqlalchemy.exc.IntegrityError: FOREIGN KEY constraint failed
```
Solution: Ensure referenced records exist before creating relationships.
## Further Reading
- [SQLAlchemy 2.0 Documentation](https://docs.sqlalchemy.org/en/20/)
- [Alembic Tutorial](https://alembic.sqlalchemy.org/en/latest/tutorial.html)
- [FastAPI with Databases](https://fastapi.tiangolo.com/tutorial/sql-databases/)