DocDoeAI / app /services /document_deletion.py
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
3bcdb36
Raw
History Blame Contribute Delete
3.38 kB
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)