feat(tags): TagService.merge guards + MergeResult + repoint stubs
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -46,6 +46,16 @@ class TagAutocompleteHit:
|
||||
image_count: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MergeResult:
|
||||
target_id: int
|
||||
target_name: str
|
||||
target_kind: str
|
||||
merged_count: int
|
||||
alias_created: bool
|
||||
source_deleted: bool
|
||||
|
||||
|
||||
class TagService:
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
@@ -259,3 +269,84 @@ class TagService:
|
||||
tag.name = new_name
|
||||
await self.session.flush()
|
||||
return tag
|
||||
|
||||
async def merge(self, source_id: int, target_id: int) -> MergeResult:
|
||||
"""Transactionally repoint every FK from source→target, optionally
|
||||
keep source's name as a tagger alias, delete source. Atomic: any
|
||||
exception rolls back via the caller's session."""
|
||||
if source_id == target_id:
|
||||
raise TagValidationError("Cannot merge a tag into itself")
|
||||
source = await self.session.get(Tag, source_id)
|
||||
if source is None:
|
||||
raise TagValidationError(f"Tag {source_id} not found")
|
||||
target = await self.session.get(Tag, target_id)
|
||||
if target is None:
|
||||
raise TagValidationError(f"Tag {target_id} not found")
|
||||
if source.kind != target.kind or (source.fandom_id or 0) != (
|
||||
target.fandom_id or 0
|
||||
):
|
||||
raise TagValidationError(
|
||||
"Tags must be the same kind and fandom to merge"
|
||||
)
|
||||
|
||||
keep_as_alias = await self._keep_as_alias(source_id)
|
||||
source_name = source.name
|
||||
source_kind = source.kind
|
||||
target_name = target.name
|
||||
target_kind = (
|
||||
target.kind.value
|
||||
if hasattr(target.kind, "value")
|
||||
else str(target.kind)
|
||||
)
|
||||
|
||||
merged_count = await self._repoint_image_tags(source_id, target_id)
|
||||
await self._repoint_rejections(source_id, target_id)
|
||||
await self._repoint_allowlist(source_id, target_id)
|
||||
await self._repoint_embedding(source_id)
|
||||
await self._repoint_aliases(source_id, target_id)
|
||||
await self._repoint_fandom_children(
|
||||
source_id, target_id, source_kind
|
||||
)
|
||||
|
||||
alias_created = False
|
||||
if keep_as_alias:
|
||||
alias_created = await self._create_protective_aliases(
|
||||
source_name, source_kind, target_id
|
||||
)
|
||||
|
||||
await self.session.delete(source)
|
||||
await self.session.flush()
|
||||
|
||||
return MergeResult(
|
||||
target_id=target_id,
|
||||
target_name=target_name,
|
||||
target_kind=target_kind,
|
||||
merged_count=merged_count,
|
||||
alias_created=alias_created,
|
||||
source_deleted=True,
|
||||
)
|
||||
|
||||
async def _repoint_image_tags(self, src: int, tgt: int) -> int:
|
||||
return 0
|
||||
|
||||
async def _repoint_rejections(self, src: int, tgt: int) -> None:
|
||||
pass
|
||||
|
||||
async def _repoint_allowlist(self, src: int, tgt: int) -> None:
|
||||
pass
|
||||
|
||||
async def _repoint_embedding(self, src: int) -> None:
|
||||
pass
|
||||
|
||||
async def _repoint_aliases(self, src: int, tgt: int) -> None:
|
||||
pass
|
||||
|
||||
async def _repoint_fandom_children(
|
||||
self, src: int, tgt: int, src_kind: TagKind
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
async def _create_protective_aliases(
|
||||
self, src_name: str, src_kind: TagKind, tgt: int
|
||||
) -> bool:
|
||||
return False
|
||||
|
||||
Reference in New Issue
Block a user