{"record":{"id":"c5b232639d732166","repo":"jamiepine/voicebox","slug":"cannot-delete-the-last-remaining-version","errorCode":null,"errorMessage":"Cannot delete the last remaining version","messagePattern":"Cannot delete the last remaining version","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"backend/routes/effects.py","lineNumber":258,"sourceCode":"        raise HTTPException(status_code=404, detail=\"Version not found\")\n    return result\n\n\n@router.delete(\"/generations/{generation_id}/versions/{version_id}\")\nasync def delete_generation_version(\n    generation_id: str,\n    version_id: str,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Delete a version. Cannot delete the last remaining version.\"\"\"\n    from ..services import versions as versions_mod\n\n    version = versions_mod.get_version(version_id, db)\n    if not version or version.generation_id != generation_id:\n        raise HTTPException(status_code=404, detail=\"Version not found\")\n\n    if not versions_mod.delete_version(version_id, db):\n        raise HTTPException(\n            status_code=400,\n            detail=\"Cannot delete the last remaining version\",\n        )\n    return {\"status\": \"deleted\"}\n","sourceCodeStart":240,"sourceCodeEnd":263,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/backend/routes/effects.py#L240-L263","documentation":"Returned as HTTP 400 by DELETE /generations/{generation_id}/versions/{version_id}. The route calls versions.delete_version(), which counts the generation's versions and returns False when count <= 1. The last remaining version cannot be deleted because every generation must keep at least one playable audio file (the default/clean version).","triggerScenarios":"Deleting the only version attached to a generation; deleting the second-to-last when the count query sees only one (race with another delete); deleting the last user-created version when no other versions exist.","commonSituations":"User trying to clear all versions of a generation through the UI; an automated cleanup script that iterates versions without leaving one; a generation that never had a clean version created and only has the derived one.","solutions":["Leave at least one version per generation; if you want it gone, delete the whole generation instead.","Disable the delete button in the UI when the versions count for that generation is 1.","To replace the audio, create a new version first, then delete the old one.","If the goal is full removal, call DELETE /generations/{id} which cascades version cleanup."],"exampleFix":"// before\nfor (const v of versions) await api.delete(`/generations/${genId}/versions/${v.id}`);\n// after: never strip the last version — delete the generation instead\nif (versions.length <= 1) {\n  await api.delete(`/generations/${genId}`);\n} else {\n  for (const v of versions.slice(0, -1)) await api.delete(`/generations/${genId}/versions/${v.id}`);\n}","handlingStrategy":"validation","validationCode":"count = (db.query(DBGenerationVersion).filter_by(generation_id=generation_id).count())\nif count <= 1:\n    raise LastVersionProtected(generation_id)  # would 400\n# safe to DELETE /generations/{gen_id}/versions/{ver_id}","typeGuard":"def has_more_than_one_version(versions: list) -> bool:\n    return len(versions) > 1","tryCatchPattern":"try:\n    client.delete(f'/generations/{gen_id}/versions/{ver_id}')\nexcept HTTPStatusError as e:\n    if e.response.status_code == 400 and 'last remaining' in e.response.json()['detail']:\n        # to remove all audio, delete the whole generation instead\n        client.delete(f'/generations/{gen_id}')\n        return\n    raise","preventionTips":["Disable the delete control when the generation has only one version.","To replace audio, create the new version first, then delete the old one.","For full removal, delete the generation (which cascades version cleanup).","Never iterate-delete down to zero versions; the business rule enforces a floor of one."],"tags":["fastapi","http-400","generations","versions","business-rule","last-of-kind","rest"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}