Ctrl K
ComunidadGuías…

update_db.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""
Curatarr — idempotent schema migration helper.

Runs the same ``init_db()`` the server runs at startup: ``create_all`` adds
newly declared tables, and ``_migrate_columns`` adds columns declared after a
table first existed (``ALTER TABLE ... ADD COLUMN``, guarded by a column
check). Existing data is left untouched. Safe to run as many times as you
want.

Use after pulling a release that adds tables or columns, or after editing
models locally and wanting your dev DB to catch up without dropping data.
(Before 2026-10 this script ran ``create_all`` alone, which never adds a
column to an existing table — only a server start did.)

Two guards, because ``init_db()`` can rebuild a table and drop a retired one:

* it refuses to run while Curatarr answers on its port — a live server (or
  the out-of-process music enricher) holding the write lock is exactly what
  made column adds fail;
* it first takes an online backup of the database next to it, verified with
  ``PRAGMA quick_check``, and names it so a bad migration can be undone. The
  newest three are kept: each is a full copy, and one per release used to
  pile up next to the live database for good.

Usage::

    python update_db.py
"""

import logging
import socket
import sqlite3
import sys
from datetime import datetime
from pathlib import Path

from src.config import settings
from src.database.connection import engine, init_db

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("DB-Update")


def _server_running() -> bool:
    """True when something answers on Curatarr's port on this machine."""
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.settimeout(0.5)
        return s.connect_ex(("127.0.0.1", int(settings.PORT))) == 0


def _backup() -> Path | None:
    """Online backup of the live database file, verified; None for a
    database that does not exist yet (a fresh install has nothing to lose)."""
    db = engine.url.database
    if engine.url.get_backend_name() != "sqlite" or not db or db == ":memory:":
        return None
    src_path = Path(db)
    if not src_path.exists():
        return None
    dest = src_path.with_name(
        f"{src_path.stem}.pre-migrate-{datetime.now():%Y%m%d_%H%M%S}{src_path.suffix}")
    src = sqlite3.connect(src_path, timeout=15)
    dst = sqlite3.connect(dest)
    try:
        with dst:
            src.backup(dst)
        if (dst.execute("PRAGMA quick_check(1)").fetchone() or ["?"])[0] != "ok":
            raise RuntimeError(f"backup {dest} failed quick_check")
    finally:
        dst.close()
        src.close()
    return dest


PRE_MIGRATE_KEEP = 3


def _rotate_pre_migrate(src_path: Path, keep: int = PRE_MIGRATE_KEEP) -> int:
    """Drop all but the newest *keep* pre-migrate snapshots of *src_path*
    (the timestamp in the name sorts lexically). Returns how many went."""
    snaps = sorted(src_path.parent.glob(f"{src_path.stem}.pre-migrate-*{src_path.suffix}"))
    dropped = 0
    for old in snaps[:-keep] if keep > 0 else snaps:
        try:
            old.unlink()
            dropped += 1
        except OSError as e:
            logger.warning("Could not remove old backup %s: %s", old, e)
    return dropped


def update_database():
    if _server_running():
        logger.error("Curatarr is running on port %s — stop it (and "
                     "scripts/music_enricher.py) before migrating.", settings.PORT)
        sys.exit(1)
    backup = _backup()
    if backup:
        dropped = _rotate_pre_migrate(Path(engine.url.database))
        logger.info("Backup written: %s%s", backup,
                    f" (removed {dropped} older)" if dropped else "")
    logger.info("Checking the database for newly-declared tables and columns...")
    init_db()
    logger.info("Done. Missing tables and columns have been added; existing data is untouched.")
    if backup:
        logger.info("If anything looks wrong, copy %s back over %s.", backup, engine.url.database)


if __name__ == "__main__":
    update_database()