Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/style.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!")
Expand Down
78 changes: 27 additions & 51 deletions src/tagstudio/core/library/alchemy/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
from pathlib import Path
from typing import TYPE_CHECKING

import sqlalchemy
import structlog
from humanfriendly import format_timespan # pyright: ignore[reportUnknownVariableType]
from sqlalchemy import (
Expand Down Expand Up @@ -669,37 +668,32 @@ def entries_count(self) -> int:
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]:
def all_entries(self, 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),
)
with Session(self.engine) as session:
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()
stmt = stmt.distinct()

entries = session.execute(stmt).scalars()
if with_joins:
entries = entries.unique()
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)
for entry in entries:
yield entry
session.expunge(entry)

@property
def tags(self) -> list[Tag]:
Expand Down Expand Up @@ -1751,30 +1745,12 @@ def get_version(self, key: str) -> int:
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."""
Expand Down
43 changes: 30 additions & 13 deletions src/tagstudio/core/library/alchemy/migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from pathlib import Path
from typing import override

import sqlalchemy
import structlog
import ujson
from sqlalchemy import Engine, and_, delete, select, text, update
Expand All @@ -21,7 +22,7 @@
)
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
Expand All @@ -46,15 +47,12 @@ def run(cls, session: Session, library_dir: Path, fmt_log: LoggingMethod) -> Non

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.
Expand Down Expand Up @@ -84,6 +82,8 @@ def required(self) -> bool:
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)
Expand All @@ -102,9 +102,6 @@ def run(self):
MigrationTo300, # changes: deletes folders
]
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
Expand Down Expand Up @@ -137,6 +134,27 @@ def run(self):
"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.

Expand Down Expand Up @@ -237,11 +255,10 @@ def run(cls, session: Session, library_dir: Path, fmt_log: LoggingMethod):
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()
Expand Down
Loading