diff --git a/docs/style.md b/docs/style.md index 901d309a..f52adf61 100644 --- a/docs/style.md +++ b/docs/style.md @@ -176,7 +176,8 @@ Observe the following key aspects of the example below: def _connect_callbacks(self): self.layout().button.clicked.connect(self._button_click_callback) self.layout().color_dropdown.currentIndexChanged.connect( - lambda idx: self._color_dropdown_callback(self.color_dropdown.itemData(idx))) + lambda idx: self._color_dropdown_callback(self.color_dropdown.itemData(idx)) + ) def _button_click_callback(self): print("Button was clicked!") diff --git a/src/tagstudio/core/library/alchemy/library.py b/src/tagstudio/core/library/alchemy/library.py index 5f650498..473774b4 100644 --- a/src/tagstudio/core/library/alchemy/library.py +++ b/src/tagstudio/core/library/alchemy/library.py @@ -14,7 +14,6 @@ from os import makedirs from pathlib import Path from typing import TYPE_CHECKING -import sqlalchemy import structlog from humanfriendly import format_timespan # pyright: ignore[reportUnknownVariableType] from sqlalchemy import ( @@ -669,37 +668,32 @@ class Library: with Session(self.engine) as session: return unwrap(session.scalar(select(func.count(Entry.id)))) - @staticmethod - def _all_entries(session: Session, with_joins: bool = False) -> Iterator[Entry]: - """Load entries without joins.""" - stmt = select(Entry) - if with_joins: - # load Entry with all joins and all tags - stmt = ( - stmt.outerjoin(Entry.text_fields) - .outerjoin(Entry.datetime_fields) - .outerjoin(Entry.tags) - ) - stmt = stmt.options( - contains_eager(Entry.text_fields), - contains_eager(Entry.datetime_fields), - contains_eager(Entry.tags), - ) - - stmt = stmt.distinct() - - entries = session.execute(stmt).scalars() - if with_joins: - entries = entries.unique() - - for entry in entries: - yield entry - session.expunge(entry) - def all_entries(self, with_joins: bool = False) -> Iterator[Entry]: """Load entries without joins.""" with Session(self.engine) as session: - yield from Library._all_entries(session, with_joins) + stmt = select(Entry) + if with_joins: + # load Entry with all joins and all tags + stmt = ( + stmt.outerjoin(Entry.text_fields) + .outerjoin(Entry.datetime_fields) + .outerjoin(Entry.tags) + ) + stmt = stmt.options( + contains_eager(Entry.text_fields), + contains_eager(Entry.datetime_fields), + contains_eager(Entry.tags), + ) + + stmt = stmt.distinct() + + entries = session.execute(stmt).scalars() + if with_joins: + entries = entries.unique() + + for entry in entries: + yield entry + session.expunge(entry) @property def tags(self) -> list[Tag]: @@ -1777,30 +1771,12 @@ class Library: Args: key(str): The key for the name of the version type to set. """ - return Library._get_version(self.engine, key) - - @staticmethod - def _get_version(engine, key: str) -> int: - with Session(engine) as session: - engine = sqlalchemy.inspect(engine) - try: - # "Version" table added in DB_VERSION 101 - if engine and engine.has_table("versions"): - version = session.scalar(select(Version).where(Version.key == key)) - assert version - return version.value - # NOTE: The "Preferences" table has been depreciated as of TagStudio 9.5.4 - # and is set to be removed in a future release. - else: - return int( - unwrap( - session.scalar( - text("SELECT value FROM preferences WHERE key == 'DB_VERSION'") - ) - ) - ) - except Exception: + with Session(self.engine) as session: + version = session.scalar(select(Version).where(Version.key == key)) + if version is None: + logger.info(f"[Library] Couldn't get version of type '{key}'") return 0 + return version.value def mirror_entry_fields(self, entries: list[Entry]) -> None: """Mirror fields among multiple Entry items.""" diff --git a/src/tagstudio/core/library/alchemy/migrations.py b/src/tagstudio/core/library/alchemy/migrations.py index 4f12358c..0224589a 100644 --- a/src/tagstudio/core/library/alchemy/migrations.py +++ b/src/tagstudio/core/library/alchemy/migrations.py @@ -6,6 +6,7 @@ from collections.abc import Callable from pathlib import Path from typing import override +import sqlalchemy import structlog import ujson from sqlalchemy import Engine, and_, delete, select, text, update @@ -21,7 +22,7 @@ from tagstudio.core.library.alchemy.constants import ( ) from tagstudio.core.library.alchemy.fields import LEGACY_FIELD_MAP, DatetimeField, TextField from tagstudio.core.library.alchemy.joins import TagParent -from tagstudio.core.library.alchemy.models import Tag, TagColorGroup, Version +from tagstudio.core.library.alchemy.models import Entry, Tag, TagColorGroup, Version from tagstudio.core.library.ignore import migrate_ext_list from tagstudio.core.utils.types import unwrap from tagstudio.qt.translations import Translations @@ -46,15 +47,12 @@ class DBMigration: class DBMigrations: def __init__(self, library_dir: Path, engine: Engine) -> None: - # TODO: Remove local import and don't make calls to private methods. - from tagstudio.core.library.alchemy.library import Library - self.library_dir = library_dir self.engine = engine # Don't check DB version when creating new library - self.loaded_db_version = Library._get_version(engine, DB_VERSION_CURRENT_KEY) - self.initial_db_version = Library._get_version(engine, DB_VERSION_INITIAL_KEY) + self.loaded_db_version = self._get_version(DB_VERSION_CURRENT_KEY) + self.initial_db_version = self._get_version(DB_VERSION_INITIAL_KEY) # ======================== Library Database Version Checking ======================= # DB_VERSION 6 is the first supported SQLite DB version. @@ -84,6 +82,8 @@ class DBMigrations: return self.loaded_db_version < DB_VERSION def run(self): + if not self.required: + return # migrate DB step by step from one version to the next # (migration_method, db_version, initial_db_version) @@ -103,9 +103,6 @@ class DBMigrations: MigrationTo400, # changes: add category_exclusions ] with Session(self.engine) as session: - if self.loaded_db_version > DB_VERSION: - return - for migration in migrations: if self.loaded_db_version < migration.version and ( migration.initial_version is None @@ -138,6 +135,27 @@ class DBMigrations: "Ran all migrations, but the DB is still not on the newest version" ) + def _get_version(self, key: str) -> int: + with Session(self.engine) as session: + inspector = sqlalchemy.inspect(self.engine) + try: + # "Version" table added in DB_VERSION 101 + if inspector and inspector.has_table("versions"): + version = session.scalar(select(Version).where(Version.key == key)) + assert version + return version.value + # "Preferences" table deprecated in TagStudio 9.5.4 + else: + return int( + unwrap( + session.scalar( + text("SELECT value FROM preferences WHERE key == 'DB_VERSION'") + ) + ) + ) + except Exception: + return 0 + def _set_version(self, session: Session, key: str, value: int) -> None: """Set a version value to the DB. @@ -238,11 +256,10 @@ class MigrationTo9(DBMigration): session.flush() logger.info(fmt_log("Added filename column to entries table")) - # TODO: Remove local import and don't make calls to private methods. # Populate the new filename column. - from tagstudio.core.library.alchemy.library import Library - - for entry in Library._all_entries(session): + # TODO: this could still break in the future through changes to the definition of Entry + entries = session.execute(select(Entry).distinct()).scalars() + for entry in entries: entry.filename = entry.path.name session.merge(entry) session.flush()