refactor(migrations): better library decoupling (#1482)

* refactor: minor improvement of version check

* refactor: don't use Library._all_entries in migrations

* chore: ruff format

* refactor: get_version split

* fix: typo

* doc: update note on deprecation of preferences table
This commit is contained in:
Jann Stute
2026-08-16 23:25:41 +02:00
committed by GitHub
parent 17cf87a4fa
commit 7e7def5a52
3 changed files with 60 additions and 66 deletions
+2 -1
View File
@@ -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!")
+28 -52
View File
@@ -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."""
@@ -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()