From 51a9c16f50ca785d810911d2d0c83fa33eb1c0ae Mon Sep 17 00:00:00 2001 From: Jann Stute <46534683+Computerdores@users.noreply.github.com> Date: Sat, 18 Jul 2026 02:35:08 +0200 Subject: [PATCH] refactor: remove dead folders table (#1444) * refactor: remove dead folders table * fix: remove folder param from all uses of Entry constructor * fix: missing filename column after migration * fix: bump db version to 300, since it is a breaking change * fix: add empty library fixture for version 202 --- .../core/library/alchemy/constants.py | 2 +- src/tagstudio/core/library/alchemy/library.py | 70 ++++++++++-------- src/tagstudio/core/library/alchemy/models.py | 14 ---- src/tagstudio/core/library/refresh.py | 2 - tests/conftest.py | 8 -- .../.TagStudio/ts_library.sqlite | Bin 0 -> 106496 bytes .../.TagStudio/ts_library.sqlite | Bin 114688 -> 122880 bytes tests/macros/test_dupe_files.py | 4 - tests/test_library.py | 11 +-- 9 files changed, 41 insertions(+), 70 deletions(-) create mode 100644 tests/fixtures/empty_libraries/DB_VERSION_202/.TagStudio/ts_library.sqlite diff --git a/src/tagstudio/core/library/alchemy/constants.py b/src/tagstudio/core/library/alchemy/constants.py index 845de2a8..73493c9a 100644 --- a/src/tagstudio/core/library/alchemy/constants.py +++ b/src/tagstudio/core/library/alchemy/constants.py @@ -9,7 +9,7 @@ JSON_FILENAME: str = "ts_library.json" DB_VERSION_CURRENT_KEY: str = "CURRENT" DB_VERSION_INITIAL_KEY: str = "INITIAL" -DB_VERSION: int = 202 +DB_VERSION: int = 300 TAG_CHILDREN_QUERY = text(""" WITH RECURSIVE ChildTags AS ( diff --git a/src/tagstudio/core/library/alchemy/library.py b/src/tagstudio/core/library/alchemy/library.py index a97f82ad..197558ac 100644 --- a/src/tagstudio/core/library/alchemy/library.py +++ b/src/tagstudio/core/library/alchemy/library.py @@ -18,7 +18,6 @@ from datetime import UTC, datetime from os import makedirs from pathlib import Path from typing import TYPE_CHECKING -from uuid import uuid4 import sqlalchemy import structlog @@ -95,7 +94,6 @@ from tagstudio.core.library.alchemy.fields import ( from tagstudio.core.library.alchemy.joins import TagEntry, TagParent from tagstudio.core.library.alchemy.models import ( Entry, - Folder, Namespace, Tag, TagAlias, @@ -234,7 +232,6 @@ class Library: library_dir: Path | None = None engine: Engine | None = None - folder: Folder | None = None included_files: set[Path] = set() def __init__(self) -> None: @@ -259,7 +256,6 @@ class Library: """Migrate JSON library data to the SQLite database.""" logger.info("Starting Library Conversion...") start_time = time.time() - folder: Folder = Folder(path=self.library_dir, uuid=str(uuid4())) # Tags for tag in json_lib.tags: @@ -312,7 +308,6 @@ class Library: [ Entry( path=entry.path / entry.filename, - folder=folder, fields=[], id=entry.id + 1, # NOTE: JSON IDs start at 0 instead of 1 date_added=datetime.now(), @@ -479,16 +474,6 @@ class Library: session.add(Version(key=DB_VERSION_CURRENT_KEY, value=DB_VERSION)) session.flush() - # add folder for current path - folder = Folder( - path=library_dir, - uuid=str(uuid4()), - ) - session.add(folder) - session.expunge(folder) - session.flush() - self.folder = folder - # Generate default .ts_ignore file try: ts_ignore_template = ( @@ -584,6 +569,7 @@ class Library: (self.__apply_db200_migration, 200, None), # changes: field tables (self.__apply_db201_migration, 201, 200), # changes: field tables (self.__apply_db202_migration, 202, None), # changes: tag_parents + (self.__apply_db300_migration, 300, None), # changes: deletes folders ] for migration, v, iv in migrations: if loaded_db_version < v and (iv is None or initial_db_version < iv): @@ -601,22 +587,6 @@ class Library: ) logger.info(f"[Library] Library migrated to DB version {DB_VERSION}") - with Session(self.engine) as session: - # TODO: the folder logic has no use and was never finished, remove it - # check if folder matching current path exists already - # NOTE: this has been causing new Folders to be created when the library is moved, since - # its introduction - self.folder = session.scalar(select(Folder).where(Folder.path == library_dir)) - if not self.folder: - folder = Folder( - path=library_dir, - uuid=str(uuid4()), - ) - session.add(folder) - session.expunge(folder) - session.commit() - self.folder = folder - # everything is fine, set the library path self.library_dir = library_dir return LibraryStatus(success=True, library_path=library_dir) @@ -906,6 +876,44 @@ class Library: session.flush() logger.info("[Library][Migration][202] Verified TagParent table data") + def __apply_db300_migration(self, session: Session, library_dir: Path): + ## remove folder_id column from entries table + # create new table in the desired scheme (without folder_id column) + session.execute( + text(""" + CREATE TABLE entries_new ( + id INTEGER NOT NULL, + path VARCHAR NOT NULL, + suffix VARCHAR NOT NULL, + date_created DATETIME, + date_modified DATETIME, + date_added DATETIME, + filename TEXT NOT NULL DEFAULT '', + PRIMARY KEY (id), + UNIQUE (path) + ) + """) + ) + session.flush() + # transfer data to new table + session.execute( + text(""" + INSERT INTO entries_new (id, path, suffix, date_created, date_modified, date_added, + filename) + SELECT id, path, suffix, date_created, date_modified, date_added, filename + FROM entries + """) + ) + # delete old table + session.execute(text("DROP TABLE entries")) + # rename new table to old table + session.execute(text("ALTER TABLE entries_new RENAME TO entries")) + session.flush() + + ## drop table "folders" + session.execute(text("DROP TABLE folders")) + session.flush() + @property def field_templates(self) -> Sequence[BaseFieldTemplate]: with Session(self.engine) as session: diff --git a/src/tagstudio/core/library/alchemy/models.py b/src/tagstudio/core/library/alchemy/models.py index 668c80f5..0b0a31c2 100644 --- a/src/tagstudio/core/library/alchemy/models.py +++ b/src/tagstudio/core/library/alchemy/models.py @@ -182,23 +182,11 @@ class Tag(Base): return self.name >= other.name -class Folder(Base): - __tablename__ = "folders" - - # TODO - implement this - id: Mapped[int] = mapped_column(primary_key=True) - path: Mapped[Path] = mapped_column(PathType, unique=True) - uuid: Mapped[str] = mapped_column(unique=True) - - class Entry(Base): __tablename__ = "entries" id: Mapped[int] = mapped_column(primary_key=True) - folder_id: Mapped[int] = mapped_column(ForeignKey("folders.id")) - folder: Mapped[Folder] = relationship("Folder") - path: Mapped[Path] = mapped_column(PathType, unique=True) filename: Mapped[str] = mapped_column() suffix: Mapped[str] = mapped_column() @@ -235,7 +223,6 @@ class Entry(Base): def __init__( self, path: Path, - folder: Folder, fields: list[BaseField], id: int | None = None, date_created: dt | None = None, @@ -244,7 +231,6 @@ class Entry(Base): ) -> None: super().__init__() self.path = path - self.folder = folder self.id = id # pyright: ignore[reportAttributeAccessIssue] self.filename = path.name self.suffix = path.suffix.lstrip(".").lower() diff --git a/src/tagstudio/core/library/refresh.py b/src/tagstudio/core/library/refresh.py index 824cae52..e0267b29 100644 --- a/src/tagstudio/core/library/refresh.py +++ b/src/tagstudio/core/library/refresh.py @@ -16,7 +16,6 @@ from tagstudio.core.library.alchemy.library import Library from tagstudio.core.library.alchemy.models import Entry from tagstudio.core.library.ignore import PATH_GLOB_FLAGS, Ignore, ignore_to_glob from tagstudio.core.utils.silent_subprocess import silent_run # pyright: ignore -from tagstudio.core.utils.types import unwrap logger = structlog.get_logger(__name__) @@ -41,7 +40,6 @@ class RefreshTracker: entries = [ Entry( path=entry_path, - folder=unwrap(self.library.folder), fields=[], date_added=dt.now(), ) diff --git a/tests/conftest.py b/tests/conftest.py index 28b1d577..6a7a3070 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,7 +20,6 @@ sys.path.insert(0, str(CWD.parent)) from tagstudio.core.constants import THUMB_CACHE_NAME, TS_FOLDER_NAME from tagstudio.core.library.alchemy.library import Library from tagstudio.core.library.alchemy.models import Entry, Tag -from tagstudio.core.utils.types import unwrap from tagstudio.qt.thumb_grid_layout import ThumbGridLayout from tagstudio.qt.ts_qt import QtDriver @@ -36,22 +35,18 @@ def file_mediatypes_library(): status = lib.open_library(Path(""), in_memory=True) assert status.success - folder = unwrap(lib.folder) entry1 = Entry( - folder=folder, path=Path("foo.png"), fields=[TextField(name="Title", value="I'm a Test Title")], ) entry2 = Entry( - folder=folder, path=Path("bar.png"), fields=[TextField(name="Title", value="I'm a Test Title")], ) entry3 = Entry( - folder=folder, path=Path("baz.apng"), fields=[TextField(name="Title", value="I'm a Test Title")], ) @@ -87,7 +82,6 @@ def library(request, library_dir: Path): # pyright: ignore lib = Library() status = lib.open_library(library_path, in_memory=True) assert status.success - folder = unwrap(lib.folder) tag = Tag( name="foo", @@ -116,7 +110,6 @@ def library(request, library_dir: Path): # pyright: ignore # default item with deterministic name entry = Entry( id=1, - folder=folder, path=Path("foo.txt"), fields=[TextField(name="Title", value="I'm a Test Title")], ) @@ -124,7 +117,6 @@ def library(request, library_dir: Path): # pyright: ignore entry2 = Entry( id=2, - folder=folder, path=Path("one/two/bar.md"), fields=[TextField(name="Title", value="I'm a Test Title")], ) diff --git a/tests/fixtures/empty_libraries/DB_VERSION_202/.TagStudio/ts_library.sqlite b/tests/fixtures/empty_libraries/DB_VERSION_202/.TagStudio/ts_library.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..db9b1c395bdb2a0373093ad8e26260dc4a64a518 GIT binary patch literal 106496 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/search_library/.TagStudio/ts_library.sqlite b/tests/fixtures/search_library/.TagStudio/ts_library.sqlite index c8489518ac94c6381e9cde53fc96c7e3b9602f30..0cfb5bed4485b806fa87f83c1a82014ffc69cb8b 100644 GIT binary patch delta 1869 zcmaKqZA@EL7{||n+xGU}+c)0n%X$j~iUX#Z2A6Cm)pc5-tB^KZMeJI7*Hud?r9??g zMizDRgOTohD9McCe90D_pv^?$Ow@#}yvUXqjb>uE_(AqT(aePSI?uU%yEW0a=iZ+C z{OJPt`~oQFL+u5>(6NUv@W)(N%OJm zxl3}c@6;rfrVjt<6C=sF1|5apRN*Dht57(yp?Uv0)HbtSfK^Nb&8TJ%_apbA@*U-) z>__ag^bqS*Oy|DIlMBhYNZRp>OPQ4wrEu_z329YsS4OJf9oZv*0yn909L*L zxPH#U`k4=uJ#?kwwBio=g8HP|OJ%9Ysm{vbiqGb)Gw>>f1wMuI3vWTaoCrRT_s3yH zXA`=U?YMW_@o|fy-e7>w*%JOhL=d8hcvu+A-%Jnm+ih+;@3!6FYv-HtPn&ovlbSjQ z&8h5JsHS56#L-k1Z_(duL+Y>Bpz2uApAdXQaRJ|g{64JUb~x=F3@`bJ$nZ!od?ft; zj6WFsPvA&6B1HYe0`ImTa`OjVczJqz`QCl@{Wedpn{R6?Px?r|!)fdH@;!DhZw&|A z%cGtHjy{i_w~~U|i)QC<&qAnuh|B^n(*ex|&10HM^;xx34OFkI+}sv7&FyA4*c7{; z*E?BuJ9Up7HFoyAQ9_E zlb4L>hsAbt+&2`C4@CrD+&4Bl?vD#ZG0;@Ty?Tv9PcEBE?1+@LNLVZS_>zR1kaE0) zGos&en&{_iwM@V)4Z#39ylgEc3rIwK#yoi!y0K(LE0;NR^Rme}68A?#f=`GVxXcpGVOkRUryGOI^7Gc3Ux z<3dm>)Ym`*7onPQvO4s~w<1+5vE!>R`X+>HRe@e!5-H;Z#zvl!%DL)j;3RSuTqKX! zWm8SAKyoyLwZ+0n2VBc6IT}O01EnJztSRGWF}G6Ua%WEToYgewA<4|5_my=ey0zGd zX4a_cv7`Q&*i2Vd(GYRJxz1oMk*O&OxpZaF+baw z)D*4fnx!i!8S>342@a~u*x6NG!CHYWIUGx}J!Iyt3j{jb;oey2hXrYo(CujN6;lJ5P6<#U0rcqD6)~9zm2x>C4IU&a R2CULZ%qp-7@p|T}_8(LhUIzdG delta 1487 zcmb7^U2Icj7{|{;+tZ%&p7wMwz^$!q2irPETRyt27zK?6m>?|ZBCDh8*P&F)y7fcZ zjcL5{BLNpK-isOe$XMdX#k9t4HzJ9pbL$3)!4MglT$p$xWHDI`#OU+3tIdWN+N4c; ze*fqH{NML^((Jd|Y*zcBsb-nuI5WO0uZH36yEV(4>&*|oh>Xav{<6^~>Jfr`g_E!3 zV8@88^njX6bieSF`}ss9)*p4h5{$l_&O~AfcUnoMQ|^Fd0787mtOK z!Q@bT$1bH!+0*W9_l7#09wp>;dLuj9ok7n|&z`p3o{%@xCYD;wa`G&;x+d@Qs1m~e zZd5k8&B?dXWL|bOtGb2%()@BDaFCtVre5OYuVkOh8Ac5U^$+zQ=?_cyrL&TYERgq! zSNv6cS9AzJ3TeTn`%ZUK$7@Hm`}m*u3~$tYrg=lts$R<3ji0D%y-flqSgjj4?m6Z3 zKzhhE&>y9yJTzJNHX>DT3^7bngXk`u0d}j z5|e%5STY<}`a<+U9vmcKW~!Z~M8n_1CVJs6Xo(V!#|BbL>F5gua1*}?Wv{tQGMK~Q z*##iiL9@O;WG)SsYJVWmlU>ot(q2|@rvStzL>7Ua;R7oYmbS>oA z6Nry)saiZ@?w6)nzRifL`5gQZW=p(PuIt)_@Fjb-4#V>TOPxptaccGgG!Xwr6s<)6 z=Y%vc10r8iDdmgs*FG+*p_4Os!Xq0H9V%@Eg;}twQ>TIh%=}6E>>Q{`*ZMUv9^3_y zUM)g1>8h!sOS533fznfB^(jU&i!AcW!ElKzWm~_Ds%mfxs}*Ta5fk|hYY;XQ=_EtF zdPFPZz)3Y}qDe*Rr+r1d%p(%A%0cG9RJy7%ueR+as|?JPEP}Y=@(pu2%%h;wQoVH? zxQ^mSBk^evRp!jLiR}XEOATg%NyYX0K$M=Ihwa3t{udc7&g0Oh89%N<-rZ7Jm#w8u zLF^#M*R3M;GMOR4J^}vYu?soHxKeb`y+W{tQM7{#_8k$H{~8(}wp9 zK0~YYn>0yI5SQ?|;2VDD>hO3x*SU0*KVSUgY_3UnOPhQ2*Yde_+|0!9=fGfW#(D!k z#mOo|QGZMSvGjv