"""Database connection management for SQLite.""" import os from collections.abc import Generator from pathlib import Path from sqlalchemy import create_engine, text from sqlalchemy.orm import Session, sessionmaker from ..paths import get_data_dir from .models import Base, Tag # Global engine instance _engine = None _SessionLocal = None def get_db_path() -> Path: """Get database path from environment or default. Resolved lazily so a packaged build writes to the per-user data dir. """ env_path = os.environ.get("PLESNA_DB_PATH") if env_path: return Path(env_path) return get_data_dir() / "database.sqlite" def _build_engine(db_path: Path): """Create a SQLAlchemy engine for the given SQLite path. Single source of truth for engine configuration. """ # Create parent directory if needed db_path.parent.mkdir(parents=True, exist_ok=True) return create_engine( f"sqlite:///{db_path}", echo=False, # Set to True for SQL debugging connect_args={"check_same_thread": False}, # Required for FastAPI ) def get_engine(db_path: Path | None = None): """Get or create SQLAlchemy engine (singleton pattern).""" global _engine if _engine is None: _engine = _build_engine(db_path or get_db_path()) return _engine def get_session_factory(engine=None): """Get or create session factory.""" global _SessionLocal if _SessionLocal is None: if engine is None: engine = get_engine() _SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) return _SessionLocal def get_session() -> Generator[Session, None, None]: """Dependency for FastAPI to get database session.""" SessionLocal = get_session_factory() session = SessionLocal() try: yield session finally: session.close() def init_db(db_path: Path | None = None) -> Path: """Initialize database: create all tables. Returns the path to the database file. """ if db_path is None: db_path = get_db_path() # Reset globals so the engine/session factory rebuild against db_path reset_connection() # Build the engine (reuses the shared configuration) and create tables engine = get_engine(db_path) Base.metadata.create_all(bind=engine) # Seed predefined tags if the table is empty _seed_tags_if_empty(engine) return db_path def _seed_tags_if_empty(engine): """Insert predefined tags if the tags table is empty.""" from ..scripts.seed_tags import PREDEFINED_TAGS session = Session(bind=engine) try: count = session.execute(text("SELECT COUNT(*) FROM tags")).scalar() if count == 0: for tag_name in PREDEFINED_TAGS: session.add(Tag(nom=tag_name)) session.commit() except Exception: session.rollback() raise finally: session.close() def reset_connection(): """Reset global connection (useful for testing).""" global _engine, _SessionLocal if _engine is not None: _engine.dispose() _engine = None _SessionLocal = None