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:
293
src/server/database/README.md
Normal file
293
src/server/database/README.md
Normal 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/)
|
||||
Reference in New Issue
Block a user