Improve migration architecture

This commit is contained in:
Xoconoch
2025-08-17 11:34:45 -06:00
parent 65d26fabac
commit 3d2c98f59b
3 changed files with 184 additions and 252 deletions

View File

@@ -3,6 +3,15 @@ import sqlite3
from pathlib import Path
from typing import Optional
from .v3_0_6 import (
check_history_3_0_6,
check_watch_playlists_3_0_6,
check_watch_artists_3_0_6,
update_history_3_0_6,
update_watch_playlists_3_0_6,
update_watch_artists_3_0_6,
)
logger = logging.getLogger(__name__)
DATA_DIR = Path("./data")
@@ -143,174 +152,8 @@ def _update_children_tables_for_history(conn: sqlite3.Connection) -> None:
logger.error("Failed migrating children history tables", exc_info=True)
def _history_needs_306(conn: sqlite3.Connection) -> bool:
"""Detect if history DB needs 3.0.6 schema (missing columns or tables)."""
# If table missing entirely, we definitely need to create it
cur = conn.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='download_history'"
)
row = cur.fetchone()
if not row:
return True
cols = _table_columns(conn, "download_history")
required = {
"id",
"download_type",
"title",
"artists",
"timestamp",
"status",
"service",
"quality_format",
"quality_bitrate",
"total_tracks",
"successful_tracks",
"failed_tracks",
"skipped_tracks",
"children_table",
"task_id",
"external_ids",
"metadata",
"release_date",
"genres",
"images",
"owner",
"album_type",
"duration_total_ms",
"explicit",
}
return not required.issubset(cols)
def _watch_playlists_needs_306(conn: sqlite3.Connection) -> bool:
cur = conn.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='watched_playlists'"
)
row = cur.fetchone()
if not row:
return True
cols = _table_columns(conn, "watched_playlists")
required = {
"spotify_id",
"name",
"owner_id",
"owner_name",
"total_tracks",
"link",
"snapshot_id",
"last_checked",
"added_at",
"is_active",
}
return not required.issubset(cols)
def _watch_artists_needs_306(conn: sqlite3.Connection) -> bool:
cur = conn.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='watched_artists'"
)
row = cur.fetchone()
if not row:
return True
cols = _table_columns(conn, "watched_artists")
required = {
"spotify_id",
"name",
"link",
"total_albums_on_spotify",
"last_checked",
"added_at",
"is_active",
"genres",
"popularity",
"image_url",
}
return not required.issubset(cols)
def _apply_history_306(conn: sqlite3.Connection) -> None:
logger.info("Applying 3.0.6 migration for history DB")
conn.executescript(
"""
CREATE TABLE IF NOT EXISTS download_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
download_type TEXT NOT NULL,
title TEXT NOT NULL,
artists TEXT,
timestamp REAL NOT NULL,
status TEXT NOT NULL,
service TEXT,
quality_format TEXT,
quality_bitrate TEXT,
total_tracks INTEGER,
successful_tracks INTEGER,
failed_tracks INTEGER,
skipped_tracks INTEGER,
children_table TEXT,
task_id TEXT,
external_ids TEXT,
metadata TEXT,
release_date TEXT,
genres TEXT,
images TEXT,
owner TEXT,
album_type TEXT,
duration_total_ms INTEGER,
explicit BOOLEAN
);
CREATE INDEX IF NOT EXISTS idx_download_history_timestamp ON download_history(timestamp);
CREATE INDEX IF NOT EXISTS idx_download_history_type_status ON download_history(download_type, status);
CREATE INDEX IF NOT EXISTS idx_download_history_task_id ON download_history(task_id);
CREATE UNIQUE INDEX IF NOT EXISTS uq_download_history_task_type_ids ON download_history(task_id, download_type, external_ids);
"""
)
# After ensuring main table, also ensure children tables
_update_children_tables_for_history(conn)
def _apply_watch_playlists_306(conn: sqlite3.Connection) -> None:
logger.info("Applying 3.0.6 migration for watch playlists DB")
conn.executescript(
"""
CREATE TABLE IF NOT EXISTS watched_playlists (
spotify_id TEXT PRIMARY KEY,
name TEXT,
owner_id TEXT,
owner_name TEXT,
total_tracks INTEGER,
link TEXT,
snapshot_id TEXT,
last_checked INTEGER,
added_at INTEGER,
is_active INTEGER DEFAULT 1
);
"""
)
def _apply_watch_artists_306(conn: sqlite3.Connection) -> None:
logger.info("Applying 3.0.6 migration for watch artists DB")
conn.executescript(
"""
CREATE TABLE IF NOT EXISTS watched_artists (
spotify_id TEXT PRIMARY KEY,
name TEXT,
link TEXT,
total_albums_on_spotify INTEGER,
last_checked INTEGER,
added_at INTEGER,
is_active INTEGER DEFAULT 1,
genres TEXT,
popularity INTEGER,
image_url TEXT
);
"""
)
def run_migrations_if_needed() -> None:
"""Detect and apply necessary migrations to align DB schema for v3.1.x.
Currently implements 3.0.6 baseline creation for history and watch DBs.
"""Detect and apply necessary migrations by version for each DB.
Idempotent by design.
"""
try:
@@ -318,11 +161,10 @@ def run_migrations_if_needed() -> None:
h_conn = _safe_connect(HISTORY_DB)
if h_conn:
try:
if _history_needs_306(h_conn):
_apply_history_306(h_conn)
else:
# Even if main table is OK, ensure children tables are up-to-date
_update_children_tables_for_history(h_conn)
if not check_history_3_0_6(h_conn):
update_history_3_0_6(h_conn)
# Ensure children tables regardless
_update_children_tables_for_history(h_conn)
h_conn.commit()
finally:
h_conn.close()
@@ -331,8 +173,8 @@ def run_migrations_if_needed() -> None:
p_conn = _safe_connect(PLAYLISTS_DB)
if p_conn:
try:
if _watch_playlists_needs_306(p_conn):
_apply_watch_playlists_306(p_conn)
if not check_watch_playlists_3_0_6(p_conn):
update_watch_playlists_3_0_6(p_conn)
p_conn.commit()
finally:
p_conn.close()
@@ -340,8 +182,8 @@ def run_migrations_if_needed() -> None:
a_conn = _safe_connect(ARTISTS_DB)
if a_conn:
try:
if _watch_artists_needs_306(a_conn):
_apply_watch_artists_306(a_conn)
if not check_watch_artists_3_0_6(a_conn):
update_watch_artists_3_0_6(a_conn)
a_conn.commit()
finally:
a_conn.close()