feat(tags): merge repoints allowlist, embedding, aliases, fandom children
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -368,18 +368,46 @@ class TagService:
|
||||
)
|
||||
|
||||
async def _repoint_allowlist(self, src: int, tgt: int) -> None:
|
||||
pass
|
||||
tgt_has = await self.session.scalar(
|
||||
select(exists().where(TagAllowlist.tag_id == tgt))
|
||||
)
|
||||
if tgt_has:
|
||||
await self.session.execute(
|
||||
text("DELETE FROM tag_allowlist WHERE tag_id = :src"),
|
||||
{"src": src},
|
||||
)
|
||||
else:
|
||||
await self.session.execute(
|
||||
update(TagAllowlist)
|
||||
.where(TagAllowlist.tag_id == src)
|
||||
.values(tag_id=tgt)
|
||||
)
|
||||
|
||||
async def _repoint_embedding(self, src: int) -> None:
|
||||
pass
|
||||
await self.session.execute(
|
||||
text(
|
||||
"DELETE FROM tag_reference_embedding WHERE tag_id = :src"
|
||||
),
|
||||
{"src": src},
|
||||
)
|
||||
|
||||
async def _repoint_aliases(self, src: int, tgt: int) -> None:
|
||||
pass
|
||||
from ..models.tag_alias import TagAlias
|
||||
|
||||
await self.session.execute(
|
||||
update(TagAlias)
|
||||
.where(TagAlias.canonical_tag_id == src)
|
||||
.values(canonical_tag_id=tgt)
|
||||
)
|
||||
|
||||
async def _repoint_fandom_children(
|
||||
self, src: int, tgt: int, src_kind: TagKind
|
||||
) -> None:
|
||||
pass
|
||||
if src_kind != TagKind.fandom:
|
||||
return
|
||||
await self.session.execute(
|
||||
update(Tag).where(Tag.fandom_id == src).values(fandom_id=tgt)
|
||||
)
|
||||
|
||||
async def _create_protective_aliases(
|
||||
self, src_name: str, src_kind: TagKind, tgt: int
|
||||
|
||||
+97
-1
@@ -1,5 +1,5 @@
|
||||
import pytest
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from backend.app.models import Tag, TagKind, image_tag
|
||||
from backend.app.models.tag_allowlist import TagAllowlist
|
||||
@@ -206,3 +206,99 @@ async def test_merge_dedups_suggestion_rejections(db):
|
||||
)
|
||||
)
|
||||
).first() is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_merge_allowlist_target_has_keeps_target_threshold(db):
|
||||
svc = TagService(db)
|
||||
a = await svc.find_or_create("SrcAL", TagKind.general)
|
||||
b = await svc.find_or_create("TgtAL", TagKind.general)
|
||||
db.add(TagAllowlist(tag_id=a.id, min_confidence=0.5))
|
||||
db.add(TagAllowlist(tag_id=b.id, min_confidence=0.9))
|
||||
await db.flush()
|
||||
await svc.merge(a.id, b.id)
|
||||
rows = (await db.execute(select(TagAllowlist))).scalars().all()
|
||||
assert len(rows) == 1
|
||||
assert rows[0].tag_id == b.id
|
||||
assert rows[0].min_confidence == 0.9
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_merge_allowlist_source_only_moves_to_target(db):
|
||||
svc = TagService(db)
|
||||
a = await svc.find_or_create("SrcAL2", TagKind.general)
|
||||
b = await svc.find_or_create("TgtAL2", TagKind.general)
|
||||
db.add(TagAllowlist(tag_id=a.id, min_confidence=0.42))
|
||||
await db.flush()
|
||||
await svc.merge(a.id, b.id)
|
||||
rows = (await db.execute(select(TagAllowlist))).scalars().all()
|
||||
assert len(rows) == 1
|
||||
assert rows[0].tag_id == b.id
|
||||
assert rows[0].min_confidence == 0.42
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_merge_deletes_source_embedding(db):
|
||||
from backend.app.models.tag_reference_embedding import (
|
||||
TagReferenceEmbedding,
|
||||
)
|
||||
|
||||
svc = TagService(db)
|
||||
a = await svc.find_or_create("SrcEmb", TagKind.general)
|
||||
b = await svc.find_or_create("TgtEmb", TagKind.general)
|
||||
db.add(
|
||||
TagReferenceEmbedding(
|
||||
tag_id=a.id,
|
||||
embedding=[0.0] * 1152,
|
||||
reference_count=1,
|
||||
model_version="v",
|
||||
)
|
||||
)
|
||||
await db.flush()
|
||||
await svc.merge(a.id, b.id)
|
||||
db.expire_all() # merge() uses Core DML; drop stale identity-map state
|
||||
remaining = await db.scalar(
|
||||
select(func.count())
|
||||
.select_from(TagReferenceEmbedding)
|
||||
.where(TagReferenceEmbedding.tag_id == a.id)
|
||||
)
|
||||
assert remaining == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_merge_repoints_existing_aliases(db):
|
||||
from backend.app.models.tag_alias import TagAlias
|
||||
|
||||
svc = TagService(db)
|
||||
a = await svc.find_or_create("SrcAlias", TagKind.general)
|
||||
b = await svc.find_or_create("TgtAlias", TagKind.general)
|
||||
db.add(
|
||||
TagAlias(
|
||||
alias_string="oldpred",
|
||||
alias_category="general",
|
||||
canonical_tag_id=a.id,
|
||||
)
|
||||
)
|
||||
await db.flush()
|
||||
await svc.merge(a.id, b.id)
|
||||
db.expire_all() # merge() uses Core DML; drop stale identity-map state
|
||||
al = (
|
||||
await db.execute(
|
||||
select(TagAlias).where(TagAlias.alias_string == "oldpred")
|
||||
)
|
||||
).scalar_one()
|
||||
assert al.canonical_tag_id == b.id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_merge_fandom_reparents_children(db):
|
||||
svc = TagService(db)
|
||||
f_src = await svc.find_or_create("FandomSrc", TagKind.fandom)
|
||||
f_tgt = await svc.find_or_create("FandomTgt", TagKind.fandom)
|
||||
child = await svc.find_or_create(
|
||||
"ChildChar", TagKind.character, fandom_id=f_src.id
|
||||
)
|
||||
await svc.merge(f_src.id, f_tgt.id)
|
||||
db.expire_all() # merge() uses Core DML; drop stale identity-map state
|
||||
refreshed = await db.get(Tag, child.id)
|
||||
assert refreshed.fandom_id == f_tgt.id
|
||||
|
||||
Reference in New Issue
Block a user