feat: add migrations to create database and add / change features down the line

This commit is contained in:
2025-10-21 10:42:52 +02:00
parent ef21a18b87
commit 0764a6b06a
3 changed files with 265 additions and 38 deletions

View File

@@ -10,8 +10,6 @@ from string import ascii_lowercase as lower
from string import digits, punctuation
from typing import Any, List, Optional, Tuple, Union
import loguru
from src import DATABASE_DIR, settings
from src.backend.db import (
CREATE_ELSA_FILES_TABLE,
@@ -30,11 +28,9 @@ from src.errors import AppPresentError, NoResultError
from src.logic import ELSA, Apparat, ApparatData, BookData, Prof
from src.logic.constants import SEMAP_MEDIA_ACCOUNTS
from src.logic.semester import Semester
from src.shared.logging import log
from src.utils.blob import create_blob
log = loguru.logger
ascii_lowercase = lower + digits + punctuation
@@ -123,6 +119,66 @@ class Database:
if not self.db_initialized:
self.checkDatabaseStatus()
self.db_initialized = True
# run migrations after initial creation to bring schema up-to-date
try:
if self.db_path is not None:
self.run_migrations()
except Exception as e:
log.error(f"Error while running migrations: {e}")
# --- Migration helpers integrated into Database ---
def _ensure_migrations_table(self, conn: sql.Connection) -> None:
cursor = conn.cursor()
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS schema_migrations (
id TEXT PRIMARY KEY,
applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
)
"""
)
conn.commit()
def _applied_migrations(self, conn: sql.Connection) -> List[str]:
cursor = conn.cursor()
cursor.execute("SELECT id FROM schema_migrations ORDER BY id")
rows = cursor.fetchall()
return [r[0] for r in rows]
def _apply_sql_file(self, conn: sql.Connection, path: Path) -> None:
log.info(f"Applying migration {path.name}")
sql_text = path.read_text(encoding="utf-8")
cursor = conn.cursor()
cursor.executescript(sql_text)
cursor.execute(
"INSERT OR REPLACE INTO schema_migrations (id) VALUES (?)", (path.name,)
)
conn.commit()
def run_migrations(self) -> None:
"""Apply unapplied .sql migrations from src/backend/migrations using this Database's connection."""
migrations_dir = Path(__file__).parent / "migrations"
if not migrations_dir.exists():
log.debug("Migrations directory does not exist, skipping migrations")
return
conn = self.connect()
try:
self._ensure_migrations_table(conn)
applied = set(self._applied_migrations(conn))
migration_files = sorted(
[p for p in migrations_dir.iterdir() if p.suffix == ".sql"]
)
for m in migration_files:
if m.name in applied:
log.debug(f"Skipping already applied migration {m.name}")
continue
self._apply_sql_file(conn, m)
finally:
conn.close()
# --- end migration helpers ---
def overwritePath(self, new_db_path: str):
log.debug("got new path, overwriting")
@@ -204,39 +260,10 @@ class Database:
"""
Create the tables in the database
"""
conn = self.connect()
cursor = conn.cursor()
cursor.execute(CREATE_TABLE_APPARAT)
cursor.execute(CREATE_TABLE_MESSAGES)
cursor.execute(CREATE_TABLE_MEDIA)
cursor.execute(CREATE_TABLE_FILES)
cursor.execute(CREATE_TABLE_PROF)
cursor.execute(CREATE_TABLE_USER)
cursor.execute(CREATE_TABLE_SUBJECTS)
cursor.execute(CREATE_ELSA_TABLE)
cursor.execute(CREATE_ELSA_FILES_TABLE)
cursor.execute(CREATE_ELSA_MEDIA_TABLE)
# Helpful indices to speed up frequent lookups and joins
cursor.execute(
"CREATE INDEX IF NOT EXISTS idx_media_app_prof ON media(app_id, prof_id);"
)
cursor.execute(
"CREATE INDEX IF NOT EXISTS idx_media_deleted ON media(deleted);"
)
cursor.execute(
"CREATE INDEX IF NOT EXISTS idx_media_available ON media(available);"
)
cursor.execute(
"CREATE INDEX IF NOT EXISTS idx_messages_remind_at ON messages(remind_at);"
)
cursor.execute(
"CREATE INDEX IF NOT EXISTS idx_semesterapparat_prof ON semesterapparat(prof_id);"
)
cursor.execute(
"CREATE INDEX IF NOT EXISTS idx_semesterapparat_appnr ON semesterapparat(appnr);"
)
conn.commit()
self.close_connection(conn)
# Bootstrapping of tables is handled via migrations. Run migrations instead
# of executing the hard-coded DDL here. Migrations are idempotent and
# contain the CREATE TABLE IF NOT EXISTS statements.
self.run_migrations()
def insertInto(self, query: str, params: Tuple) -> None:
"""