Skip to content

Commit 986dc2d

Browse files
authored
fix: relink unlinked entry to existing entry without sql error (#720)
* edited and added db functions get_entry_full_by_path & merge_entries * implemented edge case for entry existing on relinking * added test for merge_entries
1 parent 458925f commit 986dc2d

File tree

3 files changed

+93
-3
lines changed

3 files changed

+93
-3
lines changed

tagstudio/src/core/library/alchemy/library.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,32 @@ def get_entries_full(self, entry_ids: list[int] | set[int]) -> Iterator[Entry]:
490490
yield entry
491491
session.expunge(entry)
492492

493+
def get_entry_full_by_path(self, path: Path) -> Entry | None:
494+
"""Get the entry with the corresponding path."""
495+
with Session(self.engine) as session:
496+
stmt = select(Entry).where(Entry.path == path)
497+
stmt = (
498+
stmt.outerjoin(Entry.text_fields)
499+
.outerjoin(Entry.datetime_fields)
500+
.options(selectinload(Entry.text_fields), selectinload(Entry.datetime_fields))
501+
)
502+
stmt = (
503+
stmt.outerjoin(Entry.tags)
504+
.outerjoin(TagAlias)
505+
.options(
506+
selectinload(Entry.tags).options(
507+
joinedload(Tag.aliases),
508+
joinedload(Tag.parent_tags),
509+
)
510+
)
511+
)
512+
entry = session.scalar(stmt)
513+
if not entry:
514+
return None
515+
session.expunge(entry)
516+
make_transient(entry)
517+
return entry
518+
493519
@property
494520
def entries_count(self) -> int:
495521
with Session(self.engine) as session:
@@ -698,7 +724,13 @@ def search_tags(
698724

699725
return res
700726

701-
def update_entry_path(self, entry_id: int | Entry, path: Path) -> None:
727+
def update_entry_path(self, entry_id: int | Entry, path: Path) -> bool:
728+
"""Set the path field of an entry.
729+
730+
Returns True if the action succeeded and False if the path already exists.
731+
"""
732+
if self.has_path_entry(path):
733+
return False
702734
if isinstance(entry_id, Entry):
703735
entry_id = entry_id.id
704736

@@ -715,6 +747,7 @@ def update_entry_path(self, entry_id: int | Entry, path: Path) -> None:
715747

716748
session.execute(update_stmt)
717749
session.commit()
750+
return True
718751

719752
def remove_tag(self, tag: Tag):
720753
with Session(self.engine, expire_on_commit=False) as session:
@@ -1185,6 +1218,18 @@ def mirror_entry_fields(self, *entries: Entry) -> None:
11851218
value=field.value,
11861219
)
11871220

1221+
def merge_entries(self, from_entry: Entry, into_entry: Entry) -> None:
1222+
"""Add fields and tags from the first entry to the second, and then delete the first."""
1223+
for field in from_entry.fields:
1224+
self.add_field_to_entry(
1225+
entry_id=into_entry.id,
1226+
field_id=field.type_key,
1227+
value=field.value,
1228+
)
1229+
tag_ids = [tag.id for tag in from_entry.tags]
1230+
self.add_tags_to_entry(into_entry.id, tag_ids)
1231+
self.remove_entries([from_entry.id])
1232+
11881233
@property
11891234
def tag_color_groups(self) -> dict[str, list[TagColorGroup]]:
11901235
"""Return every TagColorGroup in the library."""

tagstudio/src/core/utils/missing_files.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,18 @@ def fix_missing_files(self) -> Iterator[int]:
5353
for i, entry in enumerate(self.missing_files, start=1):
5454
item_matches = self.match_missing_file(entry)
5555
if len(item_matches) == 1:
56-
logger.info("fix_missing_files", entry=entry, item_matches=item_matches)
57-
self.library.update_entry_path(entry.id, item_matches[0])
56+
logger.info(
57+
"fix_missing_files",
58+
entry=entry.path.as_posix(),
59+
item_matches=item_matches[0].as_posix(),
60+
)
61+
if not self.library.update_entry_path(entry.id, item_matches[0]):
62+
try:
63+
match = self.library.get_entry_full_by_path(item_matches[0])
64+
entry_full = self.library.get_entry_full(entry.id)
65+
self.library.merge_entries(entry_full, match)
66+
except AttributeError:
67+
continue
5868
self.files_fixed_count += 1
5969
# remove fixed file
6070
self.missing_files.remove(entry)

tagstudio/tests/test_library.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,41 @@ def test_mirror_entry_fields(library: Library, entry_full):
309309
}
310310

311311

312+
def test_merge_entries(library: Library):
313+
a = Entry(
314+
folder=library.folder,
315+
path=Path("a"),
316+
fields=[
317+
TextField(type_key=_FieldID.AUTHOR.name, value="Author McAuthorson", position=0),
318+
TextField(type_key=_FieldID.DESCRIPTION.name, value="test description", position=2),
319+
],
320+
)
321+
b = Entry(
322+
folder=library.folder,
323+
path=Path("b"),
324+
fields=[TextField(type_key=_FieldID.NOTES.name, value="test note", position=1)],
325+
)
326+
try:
327+
ids = library.add_entries([a, b])
328+
entry_a = library.get_entry_full(ids[0])
329+
entry_b = library.get_entry_full(ids[1])
330+
tag_0 = library.add_tag(Tag(id=1000, name="tag_0"))
331+
tag_1 = library.add_tag(Tag(id=1001, name="tag_1"))
332+
tag_2 = library.add_tag(Tag(id=1002, name="tag_2"))
333+
library.add_tags_to_entry(ids[0], [tag_0.id, tag_2.id])
334+
library.add_tags_to_entry(ids[1], [tag_1.id])
335+
library.merge_entries(entry_a, entry_b)
336+
assert library.has_path_entry(Path("b"))
337+
assert not library.has_path_entry(Path("a"))
338+
fields = [field.value for field in entry_a.fields]
339+
assert "Author McAuthorson" in fields
340+
assert "test description" in fields
341+
assert "test note" in fields
342+
assert b.has_tag(tag_0) and b.has_tag(tag_1) and b.has_tag(tag_2)
343+
except AttributeError:
344+
AssertionError()
345+
346+
312347
def test_remove_tag_from_entry(library, entry_full):
313348
removed_tag_id = -1
314349
for tag in entry_full.tags:

0 commit comments

Comments
 (0)