diff --git a/tagstudio/src/core/library/alchemy/enums.py b/tagstudio/src/core/library/alchemy/enums.py index 6d8f3bd6..137f0edb 100644 --- a/tagstudio/src/core/library/alchemy/enums.py +++ b/tagstudio/src/core/library/alchemy/enums.py @@ -5,6 +5,8 @@ from pathlib import Path from src.core.query_lang import AST as Query # noqa: N811 from src.core.query_lang import Constraint, ConstraintType, Parser +MAX_SQL_VARIABLES = 32766 # 32766 is the max sql bind parameter count as defined here: https://github.com/sqlite/sqlite/blob/master/src/sqliteLimit.h#L140 + class TagColor(enum.IntEnum): DEFAULT = 1 diff --git a/tagstudio/src/core/library/alchemy/library.py b/tagstudio/src/core/library/alchemy/library.py index 37b58d05..9d3df549 100644 --- a/tagstudio/src/core/library/alchemy/library.py +++ b/tagstudio/src/core/library/alchemy/library.py @@ -54,7 +54,7 @@ from ...constants import ( ) from ...enums import LibraryPrefs from .db import make_tables -from .enums import FieldTypeEnum, FilterState, SortingModeEnum, TagColor +from .enums import MAX_SQL_VARIABLES, FieldTypeEnum, FilterState, SortingModeEnum, TagColor from .fields import ( BaseField, DatetimeField, @@ -546,7 +546,11 @@ class Library: def remove_entries(self, entry_ids: list[int]) -> None: """Remove Entry items matching supplied IDs from the Library.""" with Session(self.engine) as session: - session.query(Entry).where(Entry.id.in_(entry_ids)).delete() + for sub_list in [ + entry_ids[i : i + MAX_SQL_VARIABLES] + for i in range(0, len(entry_ids), MAX_SQL_VARIABLES) + ]: + session.query(Entry).where(Entry.id.in_(sub_list)).delete() session.commit() def has_path_entry(self, path: Path) -> bool: diff --git a/tagstudio/src/core/utils/missing_files.py b/tagstudio/src/core/utils/missing_files.py index 7c54c136..08dbf08c 100644 --- a/tagstudio/src/core/utils/missing_files.py +++ b/tagstudio/src/core/utils/missing_files.py @@ -60,10 +60,7 @@ class MissingRegistry: self.missing_files.remove(entry) yield i - def execute_deletion(self) -> Iterator[int]: - for i, missing in enumerate(self.missing_files, start=1): - # TODO - optimize this by removing multiple entries at once - self.library.remove_entries([missing.id]) - yield i + def execute_deletion(self) -> None: + self.library.remove_entries(list(map(lambda missing: missing.id, self.missing_files))) self.missing_files = [] diff --git a/tagstudio/src/qt/modals/delete_unlinked.py b/tagstudio/src/qt/modals/delete_unlinked.py index da229c99..871c2d8a 100644 --- a/tagstudio/src/qt/modals/delete_unlinked.py +++ b/tagstudio/src/qt/modals/delete_unlinked.py @@ -4,7 +4,7 @@ import typing -from PySide6.QtCore import Qt, Signal +from PySide6.QtCore import Qt, QThreadPool, Signal from PySide6.QtGui import QStandardItem, QStandardItemModel from PySide6.QtWidgets import ( QHBoxLayout, @@ -15,6 +15,7 @@ from PySide6.QtWidgets import ( QWidget, ) from src.core.utils.missing_files import MissingRegistry +from src.qt.helpers.custom_runnable import CustomRunnable from src.qt.translations import Translations from src.qt.widgets.progress import ProgressWidget @@ -95,8 +96,18 @@ class DeleteUnlinkedEntriesModal(QWidget): pw = ProgressWidget( cancel_button_text=None, minimum=0, - maximum=self.tracker.missing_files_count, + maximum=0, ) Translations.translate_with_setter(pw.setWindowTitle, "entries.unlinked.delete.deleting") + Translations.translate_with_setter(pw.update_label, "entries.unlinked.delete.deleting") + pw.show() - pw.from_iterable_function(self.tracker.execute_deletion, displayed_text, self.done.emit) + r = CustomRunnable(self.tracker.execute_deletion) + QThreadPool.globalInstance().start(r) + r.done.connect( + lambda: ( + pw.hide(), + pw.deleteLater(), + self.done.emit(), + ) + )