diff --git a/docs/updates/roadmap.md b/docs/updates/roadmap.md index 17037374..5d7a0ca6 100644 --- a/docs/updates/roadmap.md +++ b/docs/updates/roadmap.md @@ -94,7 +94,7 @@ These version milestones are rough estimations for when the previous core featur - [ ] Field content search [HIGH] - [ ] Sort by date created [HIGH] - [ ] Sort by date modified [HIGH] -- [ ] Sort by filename [HIGH] +- [x] Sort by filename [HIGH] - [ ] HAS operator for composition tags [HIGH] - [ ] Search bar rework - [ ] Improved tag autocomplete [HIGH] diff --git a/src/tagstudio/core/enums.py b/src/tagstudio/core/enums.py index 31e383b6..bebe348d 100644 --- a/src/tagstudio/core/enums.py +++ b/src/tagstudio/core/enums.py @@ -82,4 +82,4 @@ class LibraryPrefs(DefaultEnum): IS_EXCLUDE_LIST = True EXTENSION_LIST = [".json", ".xmp", ".aae"] PAGE_SIZE = 500 - DB_VERSION = 8 + DB_VERSION = 9 diff --git a/src/tagstudio/core/library/alchemy/enums.py b/src/tagstudio/core/library/alchemy/enums.py index dc3b8b56..e2da7382 100644 --- a/src/tagstudio/core/library/alchemy/enums.py +++ b/src/tagstudio/core/library/alchemy/enums.py @@ -67,6 +67,8 @@ class ItemType(enum.Enum): class SortingModeEnum(enum.Enum): DATE_ADDED = "file.date_added" + FILE_NAME = "generic.filename" + PATH = "file.path" @dataclass diff --git a/src/tagstudio/core/library/alchemy/library.py b/src/tagstudio/core/library/alchemy/library.py index 9794a79d..bb932685 100644 --- a/src/tagstudio/core/library/alchemy/library.py +++ b/src/tagstudio/core/library/alchemy/library.py @@ -472,12 +472,25 @@ class Library: # Apply any post-SQL migration patches. if not is_new: + # save backup if patches will be applied + if LibraryPrefs.DB_VERSION.default != db_version: + self.library_dir = library_dir + self.save_library_backup_to_disk() + self.library_dir = None + + # schema changes first if db_version < 8: self.apply_db8_schema_changes(session) + if db_version < 9: + self.apply_db9_schema_changes(session) + + # now the data changes if db_version == 6: self.apply_repairs_for_db6(session) if db_version >= 6 and db_version < 8: self.apply_db8_default_data(session) + if db_version < 9: + self.apply_db9_filename_population(session) # Update DB_VERSION if LibraryPrefs.DB_VERSION.default > db_version: @@ -580,6 +593,29 @@ class Library: ) session.rollback() + def apply_db9_schema_changes(self, session: Session): + """Apply database schema changes introduced in DB_VERSION 9.""" + add_filename_column = text( + "ALTER TABLE entries ADD COLUMN filename TEXT NOT NULL DEFAULT ''" + ) + try: + session.execute(add_filename_column) + session.commit() + logger.info("[Library][Migration] Added filename column to entries table") + except Exception as e: + logger.error( + "[Library][Migration] Could not create filename column in entries table!", + error=e, + ) + session.rollback() + + def apply_db9_filename_population(self, session: Session): + """Populate the filename column introduced in DB_VERSION 9.""" + for entry in self.get_entries(): + session.merge(entry).filename = entry.path.name + session.commit() + logger.info("[Library][Migration] Populated filename column in entries table") + @property def default_fields(self) -> list[BaseField]: with Session(self.engine) as session: @@ -852,7 +888,7 @@ class Library: statement = statement.distinct(Entry.id) start_time = time.time() query_count = select(func.count()).select_from(statement.alias("entries")) - count_all: int = session.execute(query_count).scalar() + count_all: int = session.execute(query_count).scalar() or 0 end_time = time.time() logger.info(f"finished counting ({format_timespan(end_time - start_time)})") @@ -860,6 +896,10 @@ class Library: match search.sorting_mode: case SortingModeEnum.DATE_ADDED: sort_on = Entry.id + case SortingModeEnum.FILE_NAME: + sort_on = func.lower(Entry.filename) + case SortingModeEnum.PATH: + sort_on = func.lower(Entry.path) statement = statement.order_by(asc(sort_on) if search.ascending else desc(sort_on)) statement = statement.limit(search.limit).offset(search.offset) @@ -1371,6 +1411,8 @@ class Library: target_path, ) + logger.info("Library backup saved to disk.", path=target_path) + return target_path def get_tag(self, tag_id: int) -> Tag | None: diff --git a/src/tagstudio/core/library/alchemy/models.py b/src/tagstudio/core/library/alchemy/models.py index 5df75b73..f85a02a4 100644 --- a/src/tagstudio/core/library/alchemy/models.py +++ b/src/tagstudio/core/library/alchemy/models.py @@ -187,6 +187,7 @@ class Entry(Base): folder: Mapped[Folder] = relationship("Folder") path: Mapped[Path] = mapped_column(PathType, unique=True) + filename: Mapped[str] = mapped_column() suffix: Mapped[str] = mapped_column() date_created: Mapped[dt | None] date_modified: Mapped[dt | None] @@ -232,6 +233,7 @@ class Entry(Base): self.path = path self.folder = folder self.id = id + self.filename = path.name self.suffix = path.suffix.lstrip(".").lower() # The date the file associated with this entry was created. diff --git a/src/tagstudio/resources/translations/de.json b/src/tagstudio/resources/translations/de.json index 44b96a5c..ef32101b 100644 --- a/src/tagstudio/resources/translations/de.json +++ b/src/tagstudio/resources/translations/de.json @@ -66,6 +66,7 @@ "file.date_added": "Hinzufügungsdatum", "file.date_created": "Erstellungsdatum", "file.date_modified": "Datum geändert", + "file.path": "Dateipfad", "file.dimensions": "Abmessungen", "file.duplicates.description": "TagStudio unterstützt das Importieren von DupeGuru-Ergebnissen um Dateiduplikate zu verwalten.", "file.duplicates.dupeguru.advice": "Nach dem Kopiervorgang kann DupeGuru benutzt werden und ungewollte Dateien zu löschen. Anschließend kann TagStudios \"Unverknüpfte Einträge reparieren\" Funktion im \"Werkzeuge\" Menü benutzt werden um die nicht verknüpften Einträge zu löschen.", diff --git a/src/tagstudio/resources/translations/en.json b/src/tagstudio/resources/translations/en.json index 6be2a131..ed683b61 100644 --- a/src/tagstudio/resources/translations/en.json +++ b/src/tagstudio/resources/translations/en.json @@ -68,6 +68,7 @@ "file.date_added": "Date Added", "file.date_created": "Date Created", "file.date_modified": "Date Modified", + "file.path": "File Path", "file.dimensions": "Dimensions", "file.duplicates.description": "TagStudio supports importing DupeGuru results to manage duplicate files.", "file.duplicates.dupeguru.advice": "After mirroring, you're free to use DupeGuru to delete the unwanted files. Afterwards, use TagStudio's \"Fix Unlinked Entries\" feature in the Tools menu in order to delete the unlinked Entries.", diff --git a/tests/conftest.py b/tests/conftest.py index 989ecad6..1a119515 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -114,7 +114,8 @@ def library(request): @pytest.fixture def search_library() -> Library: lib = Library() - lib.open_library(Path(CWD / "fixtures" / "search_library")) + status = lib.open_library(Path(CWD / "fixtures" / "search_library")) + assert status.success return lib diff --git a/tests/fixtures/empty_libraries/DB_VERSION_9/.TagStudio/ts_library.sqlite b/tests/fixtures/empty_libraries/DB_VERSION_9/.TagStudio/ts_library.sqlite new file mode 100644 index 00000000..73c0a685 Binary files /dev/null and b/tests/fixtures/empty_libraries/DB_VERSION_9/.TagStudio/ts_library.sqlite differ diff --git a/tests/fixtures/search_library/.TagStudio/ts_library.sqlite b/tests/fixtures/search_library/.TagStudio/ts_library.sqlite index 6d6d85c6..b47e91da 100644 Binary files a/tests/fixtures/search_library/.TagStudio/ts_library.sqlite and b/tests/fixtures/search_library/.TagStudio/ts_library.sqlite differ diff --git a/tests/test_db_migrations.py b/tests/test_db_migrations.py index 368fb412..ffc0744b 100644 --- a/tests/test_db_migrations.py +++ b/tests/test_db_migrations.py @@ -22,6 +22,7 @@ EMPTY_LIBRARIES = "empty_libraries" str(Path(CWD.parent / FIXTURES / EMPTY_LIBRARIES / "DB_VERSION_6")), str(Path(CWD.parent / FIXTURES / EMPTY_LIBRARIES / "DB_VERSION_7")), str(Path(CWD.parent / FIXTURES / EMPTY_LIBRARIES / "DB_VERSION_8")), + str(Path(CWD.parent / FIXTURES / EMPTY_LIBRARIES / "DB_VERSION_9")), ], ) def test_library_migrations(path: str):