perf(backend): 大纲卷过滤下推 DB WHERE——list_for_project 加 volume 参数
GET /outline?volume=N 原在端点 Python 侧过滤全部章节;现把 volume 条件下推到 OutlineRepo.list_for_project 的 DB WHERE,只查该卷行(少读多余卷 + 语义正确)。 Protocol/SqlOutlineRepo/各 OutlineRepo fake 同步加 keyword-only volume;补 repo 级 下推断言测试。向后兼容:不传 volume 仍返回全部(默认 None)。
This commit is contained in:
@@ -358,8 +358,14 @@ class FakeOutlineReadRepo:
|
||||
async def get(self, project_id: uuid.UUID, chapter_no: int) -> OutlineView | None:
|
||||
return self.rows.get((project_id, chapter_no))
|
||||
|
||||
async def list_for_project(self, project_id: uuid.UUID) -> list[OutlineView]:
|
||||
views = [v for (p, _), v in self.rows.items() if p == project_id]
|
||||
async def list_for_project(
|
||||
self, project_id: uuid.UUID, *, volume: int | None = None
|
||||
) -> list[OutlineView]:
|
||||
views = [
|
||||
v
|
||||
for (p, _), v in self.rows.items()
|
||||
if p == project_id and (volume is None or v.volume == volume)
|
||||
]
|
||||
return sorted(views, key=lambda v: v.chapter_no)
|
||||
|
||||
|
||||
|
||||
@@ -38,8 +38,11 @@ class _OutlineRepo:
|
||||
async def get(self, project_id: uuid.UUID, chapter_no: int) -> OutlineView | None:
|
||||
return self._rows.get(chapter_no)
|
||||
|
||||
async def list_for_project(self, project_id: uuid.UUID) -> list[OutlineView]:
|
||||
return [self._rows[k] for k in sorted(self._rows)]
|
||||
async def list_for_project(
|
||||
self, project_id: uuid.UUID, *, volume: int | None = None
|
||||
) -> list[OutlineView]:
|
||||
rows = [self._rows[k] for k in sorted(self._rows)]
|
||||
return [v for v in rows if volume is None or v.volume == volume]
|
||||
|
||||
|
||||
class _CharRepo:
|
||||
|
||||
@@ -264,6 +264,25 @@ async def test_get_outline_with_volume_filters_to_that_volume() -> None:
|
||||
assert all(c["volume"] == 2 for c in vol2_chapters)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_outline_repo_volume_filter_pushed_into_query() -> None:
|
||||
# 卷过滤下推到 repo(DB WHERE),而非端点 Python 侧过滤:直接调 repo 断言只回该卷。
|
||||
repo = FakeOutlineReadRepo()
|
||||
pid = uuid.uuid4()
|
||||
repo.add_chapter(pid, volume=1, chapter_no=1, beats=["卷一·1"])
|
||||
repo.add_chapter(pid, volume=1, chapter_no=2, beats=["卷一·2"])
|
||||
repo.add_chapter(pid, volume=2, chapter_no=3, beats=["卷二·3"])
|
||||
|
||||
vol1 = await repo.list_for_project(pid, volume=1)
|
||||
vol2 = await repo.list_for_project(pid, volume=2)
|
||||
all_rows = await repo.list_for_project(pid)
|
||||
|
||||
assert [v.chapter_no for v in vol1] == [1, 2]
|
||||
assert all(v.volume == 1 for v in vol1)
|
||||
assert [v.chapter_no for v in vol2] == [3]
|
||||
assert [v.chapter_no for v in all_rows] == [1, 2, 3]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_outline_returns_empty_list_when_no_outline() -> None:
|
||||
project_repo = FakeProjectRepo()
|
||||
|
||||
@@ -38,7 +38,9 @@ class _EmptyOutlineRepo:
|
||||
async def get(self, project_id: uuid.UUID, chapter_no: int) -> OutlineView | None:
|
||||
return None
|
||||
|
||||
async def list_for_project(self, project_id: uuid.UUID) -> list[OutlineView]:
|
||||
async def list_for_project(
|
||||
self, project_id: uuid.UUID, *, volume: int | None = None
|
||||
) -> list[OutlineView]:
|
||||
return []
|
||||
|
||||
|
||||
|
||||
@@ -158,9 +158,7 @@ async def get_outline(
|
||||
if project is None:
|
||||
raise AppError(ErrorCode.NOT_FOUND, f"project not found: {project_id}")
|
||||
|
||||
views = await outline_repo.list_for_project(project_id)
|
||||
if volume is not None:
|
||||
views = [v for v in views if v.volume == volume]
|
||||
views = await outline_repo.list_for_project(project_id, volume=volume)
|
||||
chapters = [
|
||||
OutlineChapterView(
|
||||
no=view.chapter_no,
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user