from __future__ import annotations from sqlalchemy import delete, select, update from sqlalchemy.orm import Session from app.models.chat_session import ChatSession from app.models.document import Document from app.models.flashcard import FlashcardSet from app.models.generation import Generation from app.models.generation_cache import GenerationCache from app.models.learning_state import GeneratedResource from app.models.previous_paper import PreviousPaper from app.models.quiz import Quiz from app.models.study_profile import StudyProfile from app.models.syllabus_item import SyllabusItem from app.models.video_render_job import VideoRenderJob from app.services.file_storage import delete_upload_file def delete_document_and_derivatives(db: Session, document: Document) -> None: """Delete one document and all account data derived directly from it. Callers must perform ownership checks before invoking this service. Database changes are committed together, then the physical upload is removed. A rendered video is allowed to outlive its source, but its source reference is cleared so no stale document link remains. """ document_id = document.id user_id = document.user_id file_path = document.file_path db.execute(delete(Generation).where(Generation.document_id == document_id)) db.execute(delete(Quiz).where(Quiz.document_id == document_id)) db.execute( delete(FlashcardSet).where(FlashcardSet.document_id == document_id), ) db.execute( delete(SyllabusItem) .where(SyllabusItem.user_id == user_id) .where(SyllabusItem.source_id == document_id), ) db.execute( delete(GeneratedResource) .where(GeneratedResource.user_id == user_id) .where(GeneratedResource.source_id == document_id), ) # Delete through the ORM so related questions/messages follow their # delete-orphan cascades even when a local SQLite test DB does not enforce # database-level cascades in exactly the same way as Postgres. previous_papers = db.scalars( select(PreviousPaper) .where(PreviousPaper.user_id == user_id) .where(PreviousPaper.file_path == file_path), ).all() for paper in previous_papers: db.delete(paper) linked_sessions = db.scalars( select(ChatSession) .where(ChatSession.user_id == user_id) .where(ChatSession.source_id == document_id), ).all() for session in linked_sessions: db.delete(session) # Cache keys are hashed, so metadata is the portable source of truth across # SQLite and Postgres. Remove any legacy row whose unhashed key still # contains the document id as well. for cache_row in db.scalars(select(GenerationCache)).all(): metadata = cache_row.metadata_json or {} if ( metadata.get("document_id") == document_id or document_id in cache_row.cache_key ): db.delete(cache_row) db.execute( update(VideoRenderJob) .where(VideoRenderJob.source_document_id == document_id) .values(source_document_id=None), ) db.execute( update(StudyProfile) .where(StudyProfile.uploaded_document_id == document_id) .values(uploaded_document_id=None), ) db.delete(document) db.commit() delete_upload_file(file_path)