From f1ca52cbe887ba4288d9570f836813566807c0f4 Mon Sep 17 00:00:00 2001 From: Estrella Pan Date: Sun, 26 Jul 2026 15:36:02 +0200 Subject: [PATCH] fix(database): clear FK references before bangumi reset-all BangumiDatabase.delete_all() issued a bare DELETE FROM bangumi while torrent.bangumi_id and aria2_gid.bangumi_id rows still referenced it; with PRAGMA foreign_keys=ON the whole reset rolled back with IntegrityError, and the surviving torrent dedup rows made re-added bangumi skip episodes as already downloaded. Delete bangumi-linked torrent records (same semantics as delete_rule) and detach aria2_gid mappings before the table wipe. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019WTfJZ3EzCpY8SLhzXT6Sf --- backend/src/module/database/bangumi.py | 20 +++++++++++++++++--- backend/src/test/test_database.py | 24 ++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/backend/src/module/database/bangumi.py b/backend/src/module/database/bangumi.py index d3f48f73..75499cf6 100644 --- a/backend/src/module/database/bangumi.py +++ b/backend/src/module/database/bangumi.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Optional from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.sql import func -from sqlmodel import and_, delete, false, or_, select +from sqlmodel import and_, delete, false, or_, select, update from module.models import Bangumi, BangumiUpdate @@ -559,8 +559,22 @@ class BangumiDatabase: logger.debug("Delete bangumi id: %s.", _id) async def delete_all(self): - statement = delete(Bangumi) - await self.session.execute(statement) + # torrent / aria2_gid 行经外键引用 bangumi.id(PRAGMA foreign_keys=ON), + # 必须先清理引用,否则整表重置在约束上直接回滚。种子记录随规则一起删 + # (与 delete_rule 语义一致,重新添加后能重新下载);aria2 映射行保留 + # 但解除关联。 + from module.models.aria2 import Aria2Gid + from module.models.torrent import Torrent + + await self.session.execute( + delete(Torrent).where(Torrent.bangumi_id.is_not(None)) # type: ignore[attr-defined] + ) + await self.session.execute( + update(Aria2Gid) + .where(Aria2Gid.bangumi_id.is_not(None)) # type: ignore[attr-defined] + .values(bangumi_id=None) + ) + await self.session.execute(delete(Bangumi)) await self.session.commit() async def search_all(self) -> list[Bangumi]: diff --git a/backend/src/test/test_database.py b/backend/src/test/test_database.py index b827f598..69b8f14e 100644 --- a/backend/src/test/test_database.py +++ b/backend/src/test/test_database.py @@ -112,6 +112,30 @@ async def test_rss_database(db_session): assert result.url == rss_url +async def test_bangumi_delete_all_with_fk_references_succeeds(db_session): + """reset/all 全表重置:torrent/aria2_gid 行经外键引用 bangumi 时, + 必须先清理引用,否则 PRAGMA foreign_keys=ON 下整个删除直接回滚。""" + from sqlalchemy import text + from sqlmodel import select as sql_select + + from module.models.aria2 import Aria2Gid + + await db_session.execute(text("PRAGMA foreign_keys=ON")) + await _ensure_bangumi(db_session, 1) + db_session.add(Torrent(name="ep01", url="https://example.com/1", bangumi_id=1)) + db_session.add(Aria2Gid(gid="gid-1", bangumi_id=1)) + await db_session.commit() + + db = BangumiDatabase(db_session) + await db.delete_all() + + assert await db.search_all() == [] + torrents = (await db_session.execute(sql_select(Torrent))).scalars().all() + assert torrents == [] + gid_row = (await db_session.execute(sql_select(Aria2Gid))).scalars().one() + assert gid_row.bangumi_id is None + + # --------------------------------------------------------------------------- # TorrentDatabase qb_hash methods # ---------------------------------------------------------------------------