"""Configuration de la base de données avec SQLAlchemy. Utilise SQLAlchemy 2.0+ avec support asynchrone (aiosqlite pour SQLite). Documentation : https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html """ from collections.abc import AsyncGenerator from typing import Any from sqlalchemy import event from sqlalchemy.ext.asyncio import ( AsyncSession, async_sessionmaker, create_async_engine, ) from sqlalchemy.orm import DeclarativeBase from app.core.config import settings class Base(DeclarativeBase): """Classe de base pour tous les modèles SQLAlchemy.""" pass # Création du moteur asynchrone engine = create_async_engine( settings.DATABASE_URL, echo=settings.DEBUG, # Log SQL en mode debug future=True, # Pool de connexions pour SQLite pool_pre_ping=True, # Vérifie la connexion avant utilisation ) # Configuration spécifique pour SQLite (activation des foreign keys) @event.listens_for(engine.sync_engine, "connect") def set_sqlite_pragma(dbapi_conn: Any, connection_record: Any) -> None: """Active les contraintes de clés étrangères pour SQLite. SQLite désactive par défaut les foreign keys, il faut les activer manuellement. """ cursor = dbapi_conn.cursor() cursor.execute("PRAGMA foreign_keys=ON") cursor.close() # Session factory pour créer des sessions asynchrones AsyncSessionLocal = async_sessionmaker( engine, class_=AsyncSession, expire_on_commit=False, # Ne pas expirer les objets après commit autocommit=False, autoflush=False, ) async def get_db() -> AsyncGenerator[AsyncSession, None]: """Générateur de session de base de données pour FastAPI. Utilisé comme dépendance FastAPI pour injecter une session dans les routes. Usage: @router.get("/items") async def get_items(db: AsyncSession = Depends(get_db)): ... Yields: AsyncSession: Session SQLAlchemy asynchrone """ async with AsyncSessionLocal() as session: try: yield session await session.commit() except Exception: await session.rollback() raise finally: await session.close() async def init_db() -> None: """Initialise la base de données. Crée toutes les tables définies dans les modèles + FTS5 pour la recherche. À utiliser uniquement en développement ou pour les tests. En production, utiliser Alembic pour les migrations. """ async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) # Initialisation de la recherche full-text FTS5 await init_fts5() async def init_fts5() -> None: """Crée la table virtuelle FTS5 et les triggers de synchronisation. FTS5 permet une recherche full-text performante sur les items. Les triggers maintiennent l'index à jour automatiquement. """ from sqlalchemy import text async with engine.begin() as conn: # Supprimer l'ancienne table FTS5 si elle existe (pour recréation propre) await conn.execute(text("DROP TRIGGER IF EXISTS fts_items_insert")) await conn.execute(text("DROP TRIGGER IF EXISTS fts_items_update")) await conn.execute(text("DROP TRIGGER IF EXISTS fts_items_delete")) await conn.execute(text("DROP TABLE IF EXISTS fts_items")) # Table virtuelle FTS5 indexant nom, description, marque, modèle, notes await conn.execute(text(""" CREATE VIRTUAL TABLE fts_items USING fts5( name, description, brand, model, notes, serial_number, content='items', content_rowid='id', tokenize='unicode61 remove_diacritics 2' ) """)) # Trigger INSERT : ajouter dans FTS5 quand un item est créé await conn.execute(text(""" CREATE TRIGGER fts_items_insert AFTER INSERT ON items BEGIN INSERT INTO fts_items(rowid, name, description, brand, model, notes, serial_number) VALUES (NEW.id, NEW.name, NEW.description, NEW.brand, NEW.model, NEW.notes, NEW.serial_number); END """)) # Trigger UPDATE : mettre à jour FTS5 quand un item est modifié await conn.execute(text(""" CREATE TRIGGER fts_items_update AFTER UPDATE ON items BEGIN INSERT INTO fts_items(fts_items, rowid, name, description, brand, model, notes, serial_number) VALUES ('delete', OLD.id, OLD.name, OLD.description, OLD.brand, OLD.model, OLD.notes, OLD.serial_number); INSERT INTO fts_items(rowid, name, description, brand, model, notes, serial_number) VALUES (NEW.id, NEW.name, NEW.description, NEW.brand, NEW.model, NEW.notes, NEW.serial_number); END """)) # Trigger DELETE : supprimer de FTS5 quand un item est supprimé await conn.execute(text(""" CREATE TRIGGER fts_items_delete AFTER DELETE ON items BEGIN INSERT INTO fts_items(fts_items, rowid, name, description, brand, model, notes, serial_number) VALUES ('delete', OLD.id, OLD.name, OLD.description, OLD.brand, OLD.model, OLD.notes, OLD.serial_number); END """)) # Remplir FTS5 avec les données existantes await conn.execute(text(""" INSERT INTO fts_items(rowid, name, description, brand, model, notes, serial_number) SELECT id, name, description, brand, model, notes, serial_number FROM items """)) async def close_db() -> None: """Ferme proprement les connexions à la base de données. À appeler lors de l'arrêt de l'application. """ await engine.dispose()