Repository navigation
Expand file tree
/
Copy pathupdate_db.py
More file actions
110 lines (89 loc) · 3.92 KB
/
Copy pathupdate_db.py
File metadata and controls
110 lines (89 loc) · 3.92 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
"""
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()