fix: open libraries from v9.5.0-pr1 in newer versions (#815)

* ui: show more informative library error messages

* tests: add sqlite db migration tests

* tests: fix and refactor migration tests

* fix: apply db8 schema changes before repairing db6

* docs: add save file format change log

* chore: remove db version explanations from docstrings
This commit is contained in:
Travis Abendshien
2025-02-24 15:14:54 -08:00
committed by GitHub
parent f9ca743b64
commit b1126d5313
8 changed files with 119 additions and 21 deletions

View File

@@ -0,0 +1,46 @@
# Save Format Changes
This page outlines the various changes made the TagStudio save file format over time, sometimes referred to as the "database" or "database file".
## JSON
| First Used | Last Used | 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 |
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.
Replaced by the new SQLite format introduced in TagStudio [v9.5.0 Pre-Release 1](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr1).
## DB_VERSION 6
| First Used | Last Used | 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 |
The first public version of the SQLite save file format.
Migration from the legacy JSON format is provided via a walkthrough when opening a legacy library in TagStudio [v9.5.0 Pre-Release 1](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr1) or later.
## DB_VERSION 7
| First Used | Last Used | 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 |
### Changes
- 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.
## DB_VERSION 8
| First Used | Last Used | Format | Location |
| ------------------------------------------------------------------------------- | --------- | ------ | ----------------------------------------------- |
| [v9.5.0-PR4](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr4) | _Current_ | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
### Changes
- Adds the `color_border` column to `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)".
- Updates Neon colors to use the the new `color_border` property.

View File

@@ -184,6 +184,7 @@ class LibraryStatus:
success: bool
library_path: Path | None = None
message: str | None = None
msg_description: str | None = None
json_migration_req: bool = False
@@ -460,10 +461,12 @@ class Library:
# Apply any post-SQL migration patches.
if not is_new:
if db_version < 8:
self.apply_db8_schema_changes(session)
if db_version == 6:
self.apply_db6_patches(session)
self.apply_repairs_for_db6(session)
if db_version >= 6 and db_version < 8:
self.apply_db7_patches(session)
self.apply_db8_default_data(session)
# Update DB_VERSION
if LibraryPrefs.DB_VERSION.default > db_version:
@@ -473,11 +476,8 @@ class Library:
self.library_dir = library_dir
return LibraryStatus(success=True, library_path=library_dir)
def apply_db6_patches(self, session: Session):
"""Apply migration patches to a library with DB_VERSION 6.
DB_VERSION 6 was only used in v9.5.0-pr1.
"""
def apply_repairs_for_db6(self, session: Session):
"""Apply database repairs introduced in DB_VERSION 7."""
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.
@@ -501,11 +501,8 @@ class Library:
session.commit()
def apply_db7_patches(self, session: Session):
"""Apply migration patches to a library with DB_VERSION 7 or earlier.
DB_VERSION 7 was used from v9.5.0-pr2 to v9.5.0-pr3.
"""
def apply_db8_schema_changes(self, session: Session):
"""Apply database schema changes introduced in DB_VERSION 8."""
# TODO: Use Alembic for this part instead
# Add the missing color_border column to the TagColorGroups table.
color_border_stmt = text(
@@ -522,6 +519,8 @@ class Library:
)
session.rollback()
def apply_db8_default_data(self, session: Session):
"""Apply default data changes introduced in DB_VERSION 8."""
tag_colors: list[TagColorGroup] = default_color_groups.standard()
tag_colors += default_color_groups.pastels()
tag_colors += default_color_groups.shades()

View File

@@ -710,14 +710,16 @@ class QtDriver(DriverMixin, QObject):
app.exec()
self.shutdown()
def show_error_message(self, message: str):
self.main_window.statusbar.showMessage(message, Qt.AlignmentFlag.AlignLeft)
self.main_window.landing_widget.set_status_label(message)
self.main_window.setWindowTitle(message)
def show_error_message(self, error_name: str, error_desc: str | None = None):
self.main_window.statusbar.showMessage(error_name, Qt.AlignmentFlag.AlignLeft)
self.main_window.landing_widget.set_status_label(error_name)
self.main_window.setWindowTitle(f"{self.base_title} - {error_name}")
msg_box = QMessageBox()
msg_box.setIcon(QMessageBox.Icon.Critical)
msg_box.setText(message)
msg_box.setText(error_name)
if error_desc:
msg_box.setInformativeText(error_desc)
msg_box.setWindowTitle(Translations["window.title.error"])
msg_box.addButton(Translations["generic.close"], QMessageBox.ButtonRole.AcceptRole)
@@ -1871,12 +1873,14 @@ class QtDriver(DriverMixin, QObject):
if self.lib.library_dir:
self.close_library()
open_status: LibraryStatus = None
open_status: LibraryStatus | None = None
try:
open_status = self.lib.open_library(path)
except Exception as e:
logger.exception(e)
open_status = LibraryStatus(success=False, library_path=path, message=type(e).__name__)
open_status = LibraryStatus(
success=False, library_path=path, message=type(e).__name__, msg_description=str(e)
)
# Migration is required
if open_status.json_migration_req:
@@ -1892,7 +1896,9 @@ class QtDriver(DriverMixin, QObject):
def init_library(self, path: Path, open_status: LibraryStatus):
if not open_status.success:
self.show_error_message(
open_status.message or Translations["window.message.error_opening_library"]
error_name=open_status.message
or Translations["window.message.error_opening_library"],
error_desc=open_status.msg_description,
)
return open_status

View File

@@ -161,7 +161,7 @@ class LandingWidget(QWidget):
# self.status_pos_anim.setEndValue(self.status_label.pos())
# self.status_pos_anim.start()
def set_status_label(self, text=str):
def set_status_label(self, text: str):
"""Set the text of the status label.
Args:

View File

@@ -0,0 +1,47 @@
# Copyright (C) 2025 Travis Abendshien (CyanVoxel).
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
import shutil
from pathlib import Path
import pytest
from src.core.constants import TS_FOLDER_NAME
from src.core.library.alchemy.library import Library
CWD = Path(__file__)
FIXTURES = "fixtures"
EMPTY_LIBRARIES = "empty_libraries"
@pytest.mark.parametrize(
"path",
[
str(Path(CWD.parent / FIXTURES / EMPTY_LIBRARIES / "DB_VERSION_6")),
str(Path(CWD.parent / FIXTURES / EMPTY_LIBRARIES / "DB_VERSION_7")),
str(Path(CWD.parent / FIXTURES / EMPTY_LIBRARIES / "DB_VERSION_8")),
],
)
def test_library_migrations(path: str):
library = Library()
# Copy libraries to temp dir so modifications don't show up in version control
original_path = Path(path)
temp_path = Path(CWD.parent / FIXTURES / EMPTY_LIBRARIES / "DB_VERSION_TEMP")
temp_path.mkdir(exist_ok=True)
temp_path_ts = temp_path / TS_FOLDER_NAME
temp_path_ts.mkdir(exist_ok=True)
shutil.copy(
original_path / TS_FOLDER_NAME / Library.SQL_FILENAME,
temp_path / TS_FOLDER_NAME / Library.SQL_FILENAME,
)
try:
status = library.open_library(library_dir=temp_path)
library.close()
shutil.rmtree(temp_path)
assert status.success
except Exception as e:
library.close()
shutil.rmtree(temp_path)
raise (e)