fix(profiles): commit the whole profile delete cascade once

delete_generations_by_profile (and the version sweep under it) committed on
their own, so a failure between the generation sweep and the profile row
left the profile listed with its history already gone. Both helpers take a
commit flag now, and delete_profile passes commit=False so the single
db.commit() at its end publishes the entire cascade. Other callers keep the
default and are unchanged.
This commit is contained in:
jamiepine
2026-10-04 00:19:41 +00:00
committed by capy-ai-staging[bot]
parent 012c957f4d
commit f0f3802b79
3 changed files with 19 additions and 11 deletions
+9 -4
View File
@@ -18,7 +18,7 @@ from .. import config
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _delete_generation_children(generation_id: str, db: Session) -> None: def _delete_generation_children(generation_id: str, db: Session, commit: bool = True) -> None:
"""Remove the rows that reference a generation, plus any version audio files. """Remove the rows that reference a generation, plus any version audio files.
Story items and versions both point at the generation by a non-null FK. Story items and versions both point at the generation by a non-null FK.
@@ -28,7 +28,7 @@ def _delete_generation_children(generation_id: str, db: Session) -> None:
from . import versions as versions_mod from . import versions as versions_mod
db.query(DBStoryItem).filter_by(generation_id=generation_id).delete() db.query(DBStoryItem).filter_by(generation_id=generation_id).delete()
versions_mod.delete_versions_for_generation(generation_id, db) versions_mod.delete_versions_for_generation(generation_id, db, commit=commit)
def _get_versions_for_generations(generation_ids: list[str], db: Session) -> dict: def _get_versions_for_generations(generation_ids: list[str], db: Session) -> dict:
@@ -349,6 +349,7 @@ async def delete_failed_generations(db: Session) -> int:
async def delete_generations_by_profile( async def delete_generations_by_profile(
profile_id: str, profile_id: str,
db: Session, db: Session,
commit: bool = True,
) -> int: ) -> int:
""" """
Delete all generations for a profile. Delete all generations for a profile.
@@ -356,6 +357,9 @@ async def delete_generations_by_profile(
Args: Args:
profile_id: Profile ID profile_id: Profile ID
db: Database session db: Database session
commit: Commit at the end. Pass False when the caller owns the
transaction (e.g. deleting the profile itself) so the whole
cascade lands in one commit.
Returns: Returns:
Number of generations deleted Number of generations deleted
@@ -365,7 +369,7 @@ async def delete_generations_by_profile(
count = 0 count = 0
for generation in generations: for generation in generations:
# Delete associated version files and rows first # Delete associated version files and rows first
_delete_generation_children(generation.id, db) _delete_generation_children(generation.id, db, commit=commit)
# Delete audio file # Delete audio file
audio_path = config.resolve_storage_path(generation.audio_path) audio_path = config.resolve_storage_path(generation.audio_path)
@@ -381,7 +385,8 @@ async def delete_generations_by_profile(
db.delete(generation) db.delete(generation)
count += 1 count += 1
db.commit() if commit:
db.commit()
return count return count
+1 -1
View File
@@ -440,7 +440,7 @@ async def delete_profile(
# Generations carry a non-null FK to the profile and the history query # Generations carry a non-null FK to the profile and the history query
# inner-joins profiles, so anything left behind here becomes a row the UI # inner-joins profiles, so anything left behind here becomes a row the UI
# can never show and a .wav in data/generations the user can never reclaim. # can never show and a .wav in data/generations the user can never reclaim.
deleted_generations = await history.delete_generations_by_profile(profile_id, db) deleted_generations = await history.delete_generations_by_profile(profile_id, db, commit=False)
if deleted_generations: if deleted_generations:
logger.info("Deleted %d generations belonging to profile %s", deleted_generations, profile_id) logger.info("Deleted %d generations belonging to profile %s", deleted_generations, profile_id)
+5 -2
View File
@@ -187,7 +187,7 @@ def delete_version(version_id: str, db: Session) -> bool:
return True return True
def delete_versions_for_generation(generation_id: str, db: Session) -> int: def delete_versions_for_generation(generation_id: str, db: Session, commit: bool = True) -> int:
"""Delete all versions for a generation (used when deleting a generation). """Delete all versions for a generation (used when deleting a generation).
This runs as part of a wider cascade — deleting one generation, sweeping This runs as part of a wider cascade — deleting one generation, sweeping
@@ -196,6 +196,9 @@ def delete_versions_for_generation(generation_id: str, db: Session) -> int:
strand the caller half-deleted, so the row goes regardless and the leaked strand the caller half-deleted, so the row goes regardless and the leaked
file is logged. ``delete_version`` keeps raising, because a single file is logged. ``delete_version`` keeps raising, because a single
user-initiated delete should fail loudly. user-initiated delete should fail loudly.
Pass ``commit=False`` when the caller owns the transaction and will commit
the whole cascade itself.
""" """
versions = ( versions = (
db.query(DBGenerationVersion) db.query(DBGenerationVersion)
@@ -214,7 +217,7 @@ def delete_versions_for_generation(generation_id: str, db: Session) -> int:
) )
db.delete(v) db.delete(v)
count += 1 count += 1
if count > 0: if count > 0 and commit:
db.commit() db.commit()
return count return count