mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-01-29 22:30:57 +00:00
fix: properly delete tag_parents row when deleting tag (#1107)
This commit is contained in:
committed by
GitHub
parent
b216490311
commit
71d04254cf
@@ -14,9 +14,9 @@ Legacy (JSON) library save format versions were tied to the release version of t
|
||||
|
||||
### Versions 1.0.0 - 9.4.2
|
||||
|
||||
| Used From | Used Until | Format | Location |
|
||||
| --------- | ----------------------------------------------------------------------- | ------ | --------------------------------------------- |
|
||||
| v1.0.0 | [v9.4.2](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.4.2) | JSON | `<Library Folder>`/.TagStudio/ts_library.json |
|
||||
| Used From | Format | Location |
|
||||
| --------- | ------ | --------------------------------------------- |
|
||||
| v1.0.0 | JSON | `<Library Folder>`/.TagStudio/ts_library.json |
|
||||
|
||||
The legacy database format for public TagStudio releases [v9.1](https://github.com/TagStudioDev/TagStudio/tree/Alpha-v9.1) through [v9.4.2](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.4.2). Variations of this format had been used privately since v1.0.0.
|
||||
|
||||
@@ -48,9 +48,9 @@ These versions were used while developing the new SQLite file format, outside an
|
||||
|
||||
### Version 6
|
||||
|
||||
| Used From | Used Until | Format | Location |
|
||||
| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------ | ----------------------------------------------- |
|
||||
| [v9.5.0-pr1](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr1) | [v9.5.0-pr1](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr1) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
|
||||
| Used From | Format | Location |
|
||||
| ------------------------------------------------------------------------------- | ------ | ----------------------------------------------- |
|
||||
| [v9.5.0-pr1](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr1) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
|
||||
|
||||
The first public version of the SQLite save file format.
|
||||
|
||||
@@ -60,9 +60,9 @@ Migration from the legacy JSON format is provided via a walkthrough when opening
|
||||
|
||||
### Version 7
|
||||
|
||||
| Used From | Used Until | Format | Location |
|
||||
| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------ | ----------------------------------------------- |
|
||||
| [v9.5.0-pr2](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr2) | [v9.5.0-pr3](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr3) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
|
||||
| Used From | Format | Location |
|
||||
| ------------------------------------------------------------------------------- | ------ | ----------------------------------------------- |
|
||||
| [v9.5.0-pr2](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr2) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
|
||||
|
||||
- Repairs "Description" fields to use a TEXT_LINE key instead of a TEXT_BOX key.
|
||||
- Repairs tags that may have a disambiguation_id pointing towards a deleted tag.
|
||||
@@ -71,9 +71,9 @@ Migration from the legacy JSON format is provided via a walkthrough when opening
|
||||
|
||||
### Version 8
|
||||
|
||||
| Used From | Used Until | Format | Location |
|
||||
| ------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ------ | ----------------------------------------------- |
|
||||
| [v9.5.0-pr4](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr4) | [v9.5.1](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.1) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
|
||||
| Used From | Format | Location |
|
||||
| ------------------------------------------------------------------------------- | ------ | ----------------------------------------------- |
|
||||
| [v9.5.0-pr4](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr4) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
|
||||
|
||||
- Adds the `color_border` column to the `tag_colors` table. Used for instructing the [secondary color](../library/tag_color.md#secondary-color) to apply to a tag's border as a new optional behavior.
|
||||
- Adds three new default colors: "Burgundy (TagStudio Shades)", "Dark Teal (TagStudio Shades)", and "Dark Lavender (TagStudio Shades)".
|
||||
@@ -83,9 +83,9 @@ Migration from the legacy JSON format is provided via a walkthrough when opening
|
||||
|
||||
### Version 9
|
||||
|
||||
| Used From | Used Until | Format | Location |
|
||||
| ----------------------------------------------------------------------- | ----------------------------------------------------------------------- | ------ | ----------------------------------------------- |
|
||||
| [v9.5.2](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.2) | [v9.5.3](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.3) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
|
||||
| Used From | Format | Location |
|
||||
| ----------------------------------------------------------------------- | ------ | ----------------------------------------------- |
|
||||
| [v9.5.2](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.2) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
|
||||
|
||||
- Adds the `filename` column to the `entries` table. Used for sorting entries by filename in search results.
|
||||
|
||||
@@ -93,20 +93,20 @@ Migration from the legacy JSON format is provided via a walkthrough when opening
|
||||
|
||||
### Version 100
|
||||
|
||||
| Used From | Used Until | Format | Location |
|
||||
| ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ------ | ----------------------------------------------- |
|
||||
| [74383e3](https://github.com/TagStudioDev/TagStudio/commit/74383e3c3c12f72be1481ab0b86c7360b95c2d85) | [v9.5.4](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.4) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
|
||||
| Used From | Format | Location |
|
||||
| ---------------------------------------------------------------------------------------------------- | ------ | ----------------------------------------------- |
|
||||
| [74383e3](https://github.com/TagStudioDev/TagStudio/commit/74383e3c3c12f72be1481ab0b86c7360b95c2d85) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
|
||||
|
||||
- Introduces built-in minor versioning
|
||||
- The version number divided by 100 (and floored) constitutes the **major** version. Major version indicate breaking changes that prevent libraries from being opened in TagStudio versions older than the ones they were created in.
|
||||
- Values more precise than this ("ones" through "tens" columns) constitute the **minor** version. These indicate minor changes that don't prevent a newer library from being opened in an older version of TagStudio, as long as the major version is not also increased.
|
||||
- Swaps `parent_id` and `child_id` values in the `tag_parents` table, which have erroneously been flipped since the first SQLite DB version.
|
||||
- Swaps `parent_id` and `child_id` values in the `tag_parents` table
|
||||
|
||||
#### Version 101
|
||||
|
||||
| Used From | Used Until | Format | Location |
|
||||
| ----------------------------------------------------------------------- | ---------- | ------ | ----------------------------------------------- |
|
||||
| [v9.5.4](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.4) | _Current_ | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
|
||||
| Used From | Format | Location |
|
||||
| ----------------------------------------------------------------------- | ------ | ----------------------------------------------- |
|
||||
| [v9.5.4](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.4) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
|
||||
|
||||
- Deprecates the `preferences` table, set to be removed in a future TagStudio version.
|
||||
- Introduces the `versions` table
|
||||
@@ -115,3 +115,11 @@ Migration from the legacy JSON format is provided via a walkthrough when opening
|
||||
- `'INITIAL'` stores the database version number in which in was created
|
||||
- Pre-existing databases set this number to `100`
|
||||
- `'CURRENT'` stores the current database version number
|
||||
|
||||
#### Version 102
|
||||
|
||||
| Used From | Format | Location |
|
||||
| ----------------------------------------------------------------------- | ------ | ----------------------------------------------- |
|
||||
| [v9.5.4](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.4) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
|
||||
|
||||
- Applies repairs to the `tag_parents` table created in [version 100](#version-100), removing rows that reference tags that have been deleted.
|
||||
|
||||
@@ -11,7 +11,7 @@ JSON_FILENAME: str = "ts_library.json"
|
||||
DB_VERSION_LEGACY_KEY: str = "DB_VERSION"
|
||||
DB_VERSION_CURRENT_KEY: str = "CURRENT"
|
||||
DB_VERSION_INITIAL_KEY: str = "INITIAL"
|
||||
DB_VERSION: int = 101
|
||||
DB_VERSION: int = 102
|
||||
|
||||
TAG_CHILDREN_QUERY = text("""
|
||||
WITH RECURSIVE ChildTags AS (
|
||||
|
||||
@@ -11,7 +11,7 @@ import re
|
||||
import shutil
|
||||
import time
|
||||
import unicodedata
|
||||
from collections.abc import Iterable, Iterator, MutableSequence
|
||||
from collections.abc import Iterable, Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
from os import makedirs
|
||||
@@ -534,21 +534,23 @@ class Library:
|
||||
self.save_library_backup_to_disk()
|
||||
self.library_dir = None
|
||||
|
||||
# schema changes first
|
||||
# NOTE: Depending on the data, some data and schema changes need to be applied in
|
||||
# different orders. This chain of methods can likely be cleaned up and/or moved.
|
||||
if loaded_db_version < 8:
|
||||
self.__apply_db8_schema_changes(session)
|
||||
if loaded_db_version < 9:
|
||||
self.__apply_db9_schema_changes(session)
|
||||
|
||||
# now the data changes
|
||||
if loaded_db_version == 6:
|
||||
self.__apply_repairs_for_db6(session)
|
||||
|
||||
if loaded_db_version >= 6 and loaded_db_version < 8:
|
||||
self.__apply_db8_default_data(session)
|
||||
if loaded_db_version < 9:
|
||||
self.__apply_db9_filename_population(session)
|
||||
if loaded_db_version < 100:
|
||||
self.__apply_db100_parent_repairs(session)
|
||||
if loaded_db_version < 102:
|
||||
self.__apply_db102_repairs(session)
|
||||
|
||||
# Convert file extension list to ts_ignore file, if a .ts_ignore file does not exist
|
||||
self.migrate_sql_to_ts_ignore(library_dir)
|
||||
@@ -566,12 +568,12 @@ class Library:
|
||||
logger.info("[Library][Migration] Applying patches to DB_VERSION: 6 library...")
|
||||
with session:
|
||||
# Repair "Description" fields with a TEXT_LINE key instead of a TEXT_BOX key.
|
||||
desc_stmd = (
|
||||
desc_stmt = (
|
||||
update(ValueType)
|
||||
.where(ValueType.key == FieldID.DESCRIPTION.name)
|
||||
.values(type=FieldTypeEnum.TEXT_BOX.name)
|
||||
)
|
||||
session.execute(desc_stmd)
|
||||
session.execute(desc_stmt)
|
||||
session.flush()
|
||||
|
||||
# Repair tags that may have a disambiguation_id pointing towards a deleted tag.
|
||||
@@ -685,7 +687,16 @@ class Library:
|
||||
)
|
||||
session.execute(stmt)
|
||||
session.commit()
|
||||
logger.info("[Library][Migration] Refactored TagParent column")
|
||||
logger.info("[Library][Migration] Refactored TagParent table")
|
||||
|
||||
def __apply_db102_repairs(self, session: Session):
|
||||
"""Repair tag_parents rows with references to deleted tags."""
|
||||
with session:
|
||||
all_tag_ids: list[int] = [t.id for t in self.tags]
|
||||
stmt = delete(TagParent).where(TagParent.parent_id.not_in(all_tag_ids))
|
||||
session.execute(stmt)
|
||||
session.commit()
|
||||
logger.info("[Library][Migration] Verified TagParent table data")
|
||||
|
||||
def migrate_sql_to_ts_ignore(self, library_dir: Path):
|
||||
# Do not continue if existing '.ts_ignore' file is found
|
||||
@@ -794,7 +805,7 @@ class Library:
|
||||
entries = dict((e.id, e) for e in session.scalars(statement))
|
||||
return [entries[id] for id in entry_ids]
|
||||
|
||||
def get_entries_full(self, entry_ids: MutableSequence[int]) -> Iterator[Entry]:
|
||||
def get_entries_full(self, entry_ids: list[int] | set[int]) -> Iterator[Entry]:
|
||||
"""Load entry and join with all joins and all tags."""
|
||||
with Session(self.engine) as session:
|
||||
statement = select(Entry).where(Entry.id.in_(set(entry_ids)))
|
||||
@@ -1112,17 +1123,17 @@ class Library:
|
||||
def remove_tag(self, tag_id: int):
|
||||
with Session(self.engine, expire_on_commit=False) as session:
|
||||
try:
|
||||
child_tags = session.scalars(
|
||||
select(TagParent).where(TagParent.child_id == tag_id)
|
||||
).all()
|
||||
aliases = session.scalars(select(TagAlias).where(TagAlias.tag_id == tag_id))
|
||||
|
||||
for alias in aliases or []:
|
||||
for alias in aliases:
|
||||
session.delete(alias)
|
||||
session.flush()
|
||||
|
||||
for child_tag in child_tags or []:
|
||||
session.delete(child_tag)
|
||||
session.expunge(child_tag)
|
||||
tag_parents = session.scalars(
|
||||
select(TagParent).where(TagParent.parent_id == tag_id)
|
||||
).all()
|
||||
for tag_parent in tag_parents:
|
||||
session.delete(tag_parent)
|
||||
session.flush()
|
||||
|
||||
disam_stmt = (
|
||||
update(Tag)
|
||||
@@ -1131,6 +1142,7 @@ class Library:
|
||||
)
|
||||
session.execute(disam_stmt)
|
||||
session.flush()
|
||||
|
||||
session.query(Tag).filter_by(id=tag_id).delete()
|
||||
session.commit()
|
||||
|
||||
@@ -1397,9 +1409,9 @@ class Library:
|
||||
def add_tag(
|
||||
self,
|
||||
tag: Tag,
|
||||
parent_ids: MutableSequence[int] | None = None,
|
||||
alias_names: MutableSequence[str] | None = None,
|
||||
alias_ids: MutableSequence[int] | None = None,
|
||||
parent_ids: list[int] | set[int] | None = None,
|
||||
alias_names: list[str] | set[str] | None = None,
|
||||
alias_ids: list[int] | set[int] | None = None,
|
||||
) -> Tag | None:
|
||||
with Session(self.engine, expire_on_commit=False) as session:
|
||||
try:
|
||||
@@ -1422,7 +1434,7 @@ class Library:
|
||||
return None
|
||||
|
||||
def add_tags_to_entries(
|
||||
self, entry_ids: int | list[int], tag_ids: int | MutableSequence[int]
|
||||
self, entry_ids: int | list[int] | set[int], tag_ids: int | list[int] | set[int]
|
||||
) -> int:
|
||||
"""Add one or more tags to one or more entries.
|
||||
|
||||
@@ -1451,7 +1463,7 @@ class Library:
|
||||
return total_added
|
||||
|
||||
def remove_tags_from_entries(
|
||||
self, entry_ids: int | list[int], tag_ids: int | MutableSequence[int]
|
||||
self, entry_ids: int | list[int] | set[int], tag_ids: int | list[int] | set[int]
|
||||
) -> bool:
|
||||
"""Remove one or more tags from one or more entries."""
|
||||
entry_ids_ = [entry_ids] if isinstance(entry_ids, int) else entry_ids
|
||||
@@ -1604,16 +1616,22 @@ class Library:
|
||||
for tag in tags:
|
||||
all_tags[tag.id] = tag
|
||||
for tag in all_tags.values():
|
||||
# Sqlalchemy tracks this as a change to the parent_tags field
|
||||
tag.parent_tags = {all_tags[p] for p in all_tag_parents.get(tag.id, [])}
|
||||
# When calling session.add with this tag instance sqlalchemy will
|
||||
# attempt to create TagParents that already exist.
|
||||
try:
|
||||
# Sqlalchemy tracks this as a change to the parent_tags field
|
||||
tag.parent_tags = {all_tags[p] for p in all_tag_parents.get(tag.id, [])}
|
||||
# When calling session.add with this tag instance sqlalchemy will
|
||||
# attempt to create TagParents that already exist.
|
||||
|
||||
state: InstanceState[Tag] = inspect(tag)
|
||||
# Prevent sqlalchemy from thinking any fields are different from what's committed
|
||||
# committed_state contains original values for fields that have changed.
|
||||
# empty when no fields have changed
|
||||
state.committed_state.clear()
|
||||
state: InstanceState[Tag] = inspect(tag)
|
||||
# Prevent sqlalchemy from thinking fields are different from what's committed
|
||||
# committed_state contains original values for fields that have changed.
|
||||
# empty when no fields have changed
|
||||
state.committed_state.clear()
|
||||
except KeyError as e:
|
||||
logger.error(
|
||||
"[LIBRARY][get_tag_hierarchy] Tag referenced by TagParent does not exist!",
|
||||
error=e,
|
||||
)
|
||||
|
||||
return all_tags
|
||||
|
||||
@@ -1669,9 +1687,9 @@ class Library:
|
||||
def update_tag(
|
||||
self,
|
||||
tag: Tag,
|
||||
parent_ids: MutableSequence[int] | None = None,
|
||||
alias_names: MutableSequence[str] | None = None,
|
||||
alias_ids: MutableSequence[int] | None = None,
|
||||
parent_ids: list[int] | set[int] | None = None,
|
||||
alias_names: list[str] | set[str] | None = None,
|
||||
alias_ids: list[int] | set[int] | None = None,
|
||||
) -> None:
|
||||
"""Edit a Tag in the Library."""
|
||||
self.add_tag(tag, parent_ids, alias_names, alias_ids)
|
||||
@@ -1728,8 +1746,8 @@ class Library:
|
||||
def update_aliases(
|
||||
self,
|
||||
tag: Tag,
|
||||
alias_ids: MutableSequence[int],
|
||||
alias_names: MutableSequence[str],
|
||||
alias_ids: list[int] | set[int],
|
||||
alias_names: list[str] | set[str],
|
||||
session: Session,
|
||||
):
|
||||
prev_aliases = session.scalars(select(TagAlias).where(TagAlias.tag_id == tag.id)).all()
|
||||
@@ -1745,7 +1763,7 @@ class Library:
|
||||
alias = TagAlias(alias_name, tag.id)
|
||||
session.add(alias)
|
||||
|
||||
def update_parent_tags(self, tag: Tag, parent_ids: MutableSequence[int], session: Session):
|
||||
def update_parent_tags(self, tag: Tag, parent_ids: list[int] | set[int], session: Session):
|
||||
if tag.id in parent_ids:
|
||||
parent_ids.remove(tag.id)
|
||||
|
||||
|
||||
@@ -197,7 +197,8 @@ class TagSearchPanel(PanelWidget):
|
||||
self.search_field.setFocus()
|
||||
self.update_tags()
|
||||
|
||||
from tagstudio.qt.modals.build_tag import BuildTagPanel # here due to circular imports
|
||||
# TODO: Move this to a top-level import
|
||||
from tagstudio.qt.mixed.build_tag import BuildTagPanel # here due to circular imports
|
||||
|
||||
self.build_tag_modal: BuildTagPanel = BuildTagPanel(self.lib)
|
||||
self.add_tag_modal: PanelModal = PanelModal(
|
||||
@@ -375,7 +376,8 @@ class TagSearchPanel(PanelWidget):
|
||||
pass
|
||||
|
||||
def edit_tag(self, tag: Tag):
|
||||
from tagstudio.qt.modals.build_tag import BuildTagPanel
|
||||
# TODO: Move this to a top-level import
|
||||
from tagstudio.qt.mixed.build_tag import BuildTagPanel
|
||||
|
||||
def callback(btp: BuildTagPanel):
|
||||
self.lib.update_tag(
|
||||
|
||||
Reference in New Issue
Block a user