From 95e2fe7b4449951c385e35a2e13f0c1925f1f98e Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Sat, 4 Jul 2026 13:35:40 -0700 Subject: [PATCH] fix: remove invalid child_id relationships from tag_parents (#1423) * fix: remove invalid child_id relationships from tag_parents * fix: use single statements in DB 102 & 202 migrations * fix: remove unnecessary session.scalars() from migrations --- docs/library-changes.md | 8 ++++++ .../core/library/alchemy/constants.py | 2 +- src/tagstudio/core/library/alchemy/library.py | 27 ++++++++++++------ .../.TagStudio/ts_library.sqlite | Bin 114688 -> 114688 bytes 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/docs/library-changes.md b/docs/library-changes.md index 47fa291f..f68ca0b3 100644 --- a/docs/library-changes.md +++ b/docs/library-changes.md @@ -193,3 +193,11 @@ Migration from the legacy JSON format is provided via a walkthrough when opening - Drops `type_key` columns from `text_fields` and `datetime_fields` tables. - Enforces column positions for `text_fields` and `datetime_fields` tables. + +#### Version 202 + +| Added in Commit | Introduced in Release | Format | +| --------------- | ----------------------------------------------------------------------- | ------ | +| | [v9.6.1](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.6.1) | SQLite | + +- Applies repairs to the `tag_parents` table, removing rows that reference child tags that have been deleted. diff --git a/src/tagstudio/core/library/alchemy/constants.py b/src/tagstudio/core/library/alchemy/constants.py index efe03aa2..845de2a8 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 = 201 +DB_VERSION: int = 202 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 d4b9cfac..d7cf6905 100644 --- a/src/tagstudio/core/library/alchemy/library.py +++ b/src/tagstudio/core/library/alchemy/library.py @@ -571,12 +571,16 @@ class Library: self.__apply_db103_migration(session) if loaded_db_version < 104: # changes: deletes preferences - self.__apply_db104_migrations(session, library_dir) + self.__apply_db104_migration(session, library_dir) if loaded_db_version < 200: - self.__apply_db200_migrations(session) # changes: field tables + self.__apply_db200_migration(session) if initial_db_version < 200 and loaded_db_version < 201: - self.__apply_db201_migrations(session) + # changes: field tables + self.__apply_db201_migration(session) + if loaded_db_version < 202: + # changes: tag_parents + self.__apply_db202_migration(session) session.execute( text("CREATE INDEX IF NOT EXISTS idx_tags_name_shorthand ON tags (name, shorthand)") @@ -719,8 +723,7 @@ class Library: def __apply_db102_migration(self, session: Session): """Migrate DB to DB_VERSION 102.""" with session: - all_tag_ids = session.scalars(text("SELECT DISTINCT id FROM tags")).all() - stmt = delete(TagParent).where(TagParent.parent_id.not_in(all_tag_ids)) + stmt = delete(TagParent).where(TagParent.parent_id.not_in(select(Tag.id).distinct())) session.execute(stmt) session.commit() logger.info("[Library][Migration] Verified TagParent table data") @@ -754,7 +757,7 @@ class Library: ) session.rollback() - def __apply_db104_migrations(self, session: Session, library_dir: Path): + def __apply_db104_migration(self, session: Session, library_dir: Path): """Migrate DB from DB_VERSION 103 to 104.""" # Convert file extension list to ts_ignore file, if a .ts_ignore file does not exist self.__migrate_sql_to_ts_ignore(library_dir) @@ -779,7 +782,7 @@ class Library: with open(ts_ignore, "w") as f: f.write(migrate_ext_list(extensions, is_exclude_list)) - def __apply_db200_migrations(self, session: Session): + def __apply_db200_migration(self, session: Session): """Migrate DB to DB_VERSION 200.""" with session: # Drop unused 'boolean_fields' and 'value_type' tables @@ -866,7 +869,7 @@ class Library: session.commit() - def __apply_db201_migrations(self, session: Session): + def __apply_db201_migration(self, session: Session): """Migrate DB to DB_VERSION 201.""" with session: create_text_fields_table = text(""" @@ -917,6 +920,14 @@ class Library: session.commit() + def __apply_db202_migration(self, session: Session): + """Migrate DB to DB_VERSION 202.""" + with session: + stmt = delete(TagParent).where(TagParent.child_id.not_in(select(Tag.id).distinct())) + session.execute(stmt) + session.commit() + logger.info("[Library][Migration] Verified TagParent table data") + @property def field_templates(self) -> Sequence[BaseFieldTemplate]: with Session(self.engine) as session: diff --git a/tests/fixtures/search_library/.TagStudio/ts_library.sqlite b/tests/fixtures/search_library/.TagStudio/ts_library.sqlite index 5389a26e1071e8c0fcc40a95bfab9779f0bb0c6d..c8489518ac94c6381e9cde53fc96c7e3b9602f30 100644 GIT binary patch delta 30 mcmZo@U~gz(pCHZnZK8}b