feat(tags): merge repoints image_tag and tag_suggestion_rejection with dedup
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -327,10 +327,45 @@ class TagService:
|
||||
)
|
||||
|
||||
async def _repoint_image_tags(self, src: int, tgt: int) -> int:
|
||||
return 0
|
||||
"""Drop source links whose image already links target, then move
|
||||
the rest. Returns the number actually moved."""
|
||||
await self.session.execute(
|
||||
image_tag.delete().where(
|
||||
and_(
|
||||
image_tag.c.tag_id == src,
|
||||
image_tag.c.image_record_id.in_(
|
||||
select(image_tag.c.image_record_id).where(
|
||||
image_tag.c.tag_id == tgt
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
res = await self.session.execute(
|
||||
image_tag.update()
|
||||
.where(image_tag.c.tag_id == src)
|
||||
.values(tag_id=tgt)
|
||||
)
|
||||
return res.rowcount or 0
|
||||
|
||||
async def _repoint_rejections(self, src: int, tgt: int) -> None:
|
||||
pass
|
||||
from ..models.tag_suggestion_rejection import TagSuggestionRejection
|
||||
|
||||
await self.session.execute(
|
||||
text(
|
||||
"DELETE FROM tag_suggestion_rejection r "
|
||||
"WHERE r.tag_id = :src AND EXISTS ("
|
||||
" SELECT 1 FROM tag_suggestion_rejection r2 "
|
||||
" WHERE r2.tag_id = :tgt "
|
||||
" AND r2.image_record_id = r.image_record_id)"
|
||||
),
|
||||
{"src": src, "tgt": tgt},
|
||||
)
|
||||
await self.session.execute(
|
||||
update(TagSuggestionRejection)
|
||||
.where(TagSuggestionRejection.tag_id == src)
|
||||
.values(tag_id=tgt)
|
||||
)
|
||||
|
||||
async def _repoint_allowlist(self, src: int, tgt: int) -> None:
|
||||
pass
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import pytest
|
||||
from sqlalchemy import select
|
||||
|
||||
from backend.app.models import Tag, TagKind, image_tag
|
||||
from backend.app.models.tag_allowlist import TagAllowlist
|
||||
@@ -129,3 +130,79 @@ async def test_merge_empty_tags_returns_result(db):
|
||||
assert result.alias_created is False
|
||||
assert result.source_deleted is True
|
||||
assert await db.get(Tag, a.id) is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_merge_moves_image_tags_and_counts(db):
|
||||
svc = TagService(db)
|
||||
a = await svc.find_or_create("SrcIT", TagKind.general)
|
||||
b = await svc.find_or_create("TgtIT", TagKind.general)
|
||||
i1, i2 = await _img(db), await _img(db)
|
||||
await svc.add_to_image(i1, a.id, source="manual")
|
||||
await svc.add_to_image(i2, a.id, source="manual")
|
||||
result = await svc.merge(a.id, b.id)
|
||||
assert result.merged_count == 2
|
||||
rows = (
|
||||
await db.execute(
|
||||
image_tag.select().where(image_tag.c.tag_id == b.id)
|
||||
)
|
||||
).all()
|
||||
assert {r.image_record_id for r in rows} == {i1, i2}
|
||||
assert (
|
||||
await db.execute(
|
||||
image_tag.select().where(image_tag.c.tag_id == a.id)
|
||||
)
|
||||
).first() is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_merge_dedups_image_tag_when_image_has_both(db):
|
||||
svc = TagService(db)
|
||||
a = await svc.find_or_create("SrcDup", TagKind.general)
|
||||
b = await svc.find_or_create("TgtDup", TagKind.general)
|
||||
img = await _img(db)
|
||||
await svc.add_to_image(img, a.id, source="manual")
|
||||
await svc.add_to_image(img, b.id, source="manual")
|
||||
result = await svc.merge(a.id, b.id)
|
||||
# The image already had the target → that source row is dropped, not moved.
|
||||
assert result.merged_count == 0
|
||||
rows = (
|
||||
await db.execute(
|
||||
image_tag.select().where(
|
||||
image_tag.c.image_record_id == img
|
||||
)
|
||||
)
|
||||
).all()
|
||||
assert [r.tag_id for r in rows] == [b.id]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_merge_dedups_suggestion_rejections(db):
|
||||
from backend.app.models.tag_suggestion_rejection import (
|
||||
TagSuggestionRejection,
|
||||
)
|
||||
|
||||
svc = TagService(db)
|
||||
a = await svc.find_or_create("SrcRej", TagKind.general)
|
||||
b = await svc.find_or_create("TgtRej", TagKind.general)
|
||||
i1, i2 = await _img(db), await _img(db)
|
||||
db.add(TagSuggestionRejection(image_record_id=i1, tag_id=a.id))
|
||||
db.add(TagSuggestionRejection(image_record_id=i2, tag_id=a.id))
|
||||
db.add(TagSuggestionRejection(image_record_id=i1, tag_id=b.id))
|
||||
await db.flush()
|
||||
await svc.merge(a.id, b.id)
|
||||
rej = (
|
||||
await db.execute(
|
||||
select(TagSuggestionRejection).where(
|
||||
TagSuggestionRejection.tag_id == b.id
|
||||
)
|
||||
)
|
||||
).scalars().all()
|
||||
assert {r.image_record_id for r in rej} == {i1, i2}
|
||||
assert (
|
||||
await db.execute(
|
||||
select(TagSuggestionRejection).where(
|
||||
TagSuggestionRejection.tag_id == a.id
|
||||
)
|
||||
)
|
||||
).first() is None
|
||||
|
||||
Reference in New Issue
Block a user