diff --git a/docs/updates/roadmap.md b/docs/updates/roadmap.md index 170373746..5d7a0ca67 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 2a11a12fd..a8739d26b 100644 --- a/src/tagstudio/core/enums.py +++ b/src/tagstudio/core/enums.py @@ -72,4 +72,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 dc3b8b56b..e2da73823 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 9794a79d6..bb9326854 100644 --- a/src/tagstudio/core/library/alchemy/library.py +++ b/src/tagstudio/core/library/alchemy/library.py @@ -472,12 +472,25 @@ def open_sqlite_library(self, library_dir: Path, is_new: bool) -> LibraryStatus: # 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 @@ def apply_db8_default_data(self, session: Session): ) 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 @@ def search_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 @@ def search_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 @@ def save_library_backup_to_disk(self) -> Path: 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 5df75b73e..f85a02a44 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 @@ def __init__( 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 72ed7eef1..e93bfe26e 100644 --- a/src/tagstudio/resources/translations/de.json +++ b/src/tagstudio/resources/translations/de.json @@ -63,6 +63,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 d1dc2caa2..1d34aa2ca 100644 --- a/src/tagstudio/resources/translations/en.json +++ b/src/tagstudio/resources/translations/en.json @@ -59,6 +59,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 989ecad66..1a1195150 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 000000000..73c0a685e 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 6d6d85c60..b47e91da9 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 368fb4126..ffc0744bd 100644 --- a/tests/test_db_migrations.py +++ b/tests/test_db_migrations.py @@ -22,6 +22,7 @@ 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):