Coverage for seedboxsync/core/database/migration.py: 67%
54 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-26 17:14 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-26 17:14 +0000
1#
2# Copyright (C) 2015-2026 Guillaume Kulakowski <guillaume@kulakowski.fr>
3#
4# For the full copyright and license information, please view the LICENSE
5# file that was distributed with this source code.
6#
8"""Database schema migration management."""
10from pathlib import Path
11from flask import Flask
12from peewee import SqliteDatabase
13from playhouse.migrations import Runner
14from seedboxsync.core import utils
15from seedboxsync.core.database.models import ApiKey, Download, SeedboxSync, TaskStatus, Torrent, User
17database = SqliteDatabase(str(utils.get_database_path_from_paths()))
18models = [ApiKey, Download, SeedboxSync, TaskStatus, Torrent, User]
19MIGRATION_PATH = str(Path(__file__).parent / "migrations")
21# Map each legacy database version to the latest equivalent migration
22# managed by the Peewee migration runner.
23LEGACY_MIGRATION_MAP: dict[int, str | None] = {
24 1: "0001_initial",
25 2: None,
26 3: "0002_torrent_nullable_announce",
27 4: "0003_add_taskstatus",
28}
31class DatabaseMigration:
32 """Manage database schema migrations."""
34 def __init__(self, app: Flask, db: SqliteDatabase) -> None:
35 """
36 Initialize the database migration manager.
38 Args:
39 app: Flask application instance.
40 db: Peewee SQLite database instance.
41 """
42 self.app = app
43 self.db = db
44 self.runner = Runner(self.db, directory=MIGRATION_PATH)
46 def upgrade(self) -> None:
47 """
48 Upgrade the database schema to the latest available migration.
50 Existing databases using the legacy integer-based migration system
51 are first converted to the Peewee migration history format.
52 """
53 self._migrate_legacy_database()
54 with self.app.app_context():
55 self._run_migrations()
56 self._set_last_migration()
58 def _run_migrations(self) -> None:
59 """Apply all pending Peewee migrations."""
60 applied = self.runner.up()
62 for migration in applied:
63 self.app.logger.info("Applied database migration: %s", migration)
65 def _set_last_migration(self) -> None:
66 """Store the latest available migration name in the Flask configuration."""
67 migrations = self.runner.status()
68 if migrations:
69 self.app.config["LAST_MIGRATION"] = migrations[-1].name
71 def _migrate_legacy_database(self) -> None:
72 """
73 Convert a database using the legacy migration system.
75 Legacy SeedboxSync releases stored an integer database version in the
76 ``seedboxsync`` table. The corresponding Peewee migrations are marked
77 as already applied without executing them again.
79 Databases that do not contain a legacy version are left untouched.
80 """
81 db_version = self._get_legacy_database_version()
83 if db_version is None:
84 return
86 self.app.logger.info("Legacy database detected at version %d; converting migration history to Peewee migrations.", db_version)
87 for version, migration_name in LEGACY_MIGRATION_MAP.items():
88 if version > db_version:
89 break
90 if migration_name is None:
91 continue
92 self.app.logger.info("Marking legacy database version %d as migration '%s' already applied.", version, migration_name)
93 self.runner.fake(migration_name)
95 self._remove_legacy_database_version()
97 def _get_legacy_database_version(self) -> int | None:
98 """
99 Return the legacy database version if one exists.
101 Returns:
102 The legacy database version, or ``None`` when the database does
103 not use the legacy migration system.
104 """
105 if "seedboxsync" not in self.db.get_tables():
106 return None
108 version = SeedboxSync.select(SeedboxSync.value).where(SeedboxSync.key == "db_version").first()
110 if version is None or not version.value:
111 return None
113 try:
114 return int(version.value)
115 except (TypeError, ValueError) as exc:
116 raise RuntimeError(f"Invalid legacy database version: {version.value!r}") from exc
118 def _remove_legacy_database_version(self) -> None:
119 """Remove the obsolete legacy database version entry."""
120 (SeedboxSync.delete().where(SeedboxSync.key == "db_version").execute())
121 self.app.logger.debug("Removed legacy database version metadata.")