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,74 @@
"""Base SQLAlchemy declarative base for all database models.
This module provides the base class that all ORM models inherit from,
along with common functionality and mixins.
"""
from datetime import datetime
from typing import Any
from sqlalchemy import DateTime, func
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
"""Base class for all SQLAlchemy ORM models.
Provides common functionality and type annotations for all models.
All models should inherit from this class.
"""
pass
class TimestampMixin:
"""Mixin to add created_at and updated_at timestamp columns.
Automatically tracks when records are created and updated.
Use this mixin for models that need audit timestamps.
Attributes:
created_at: Timestamp when record was created
updated_at: Timestamp when record was last updated
"""
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
nullable=False,
doc="Timestamp when record was created"
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
doc="Timestamp when record was last updated"
)
class SoftDeleteMixin:
"""Mixin to add soft delete functionality.
Instead of deleting records, marks them as deleted with a timestamp.
Useful for maintaining audit trails and allowing recovery.
Attributes:
deleted_at: Timestamp when record was soft deleted, None if active
"""
deleted_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True),
nullable=True,
default=None,
doc="Timestamp when record was soft deleted"
)
@property
def is_deleted(self) -> bool:
"""Check if record is soft deleted."""
return self.deleted_at is not None
def soft_delete(self) -> None:
"""Mark record as deleted without removing from database."""
self.deleted_at = datetime.utcnow()
def restore(self) -> None:
"""Restore a soft deleted record."""
self.deleted_at = None