Migrationen versioniert: migrations.py mit PRAGMA user_version
- db.py enthält nur noch Schema (neuester Stand), Verbindungen und Start - Neue Datenbank: direkt im neuesten Schema, erste Liste "Zuhause", Version = Anzahl der Schritte - Bestehende Datenbank: nur fehlende Schritte, jeder genau einmal - Bisherige Einzel-Umstellungen als Schritt 1 "Altbestand" (prüfen weiter selbst, was nötig ist); künftige Schritte werden einfach angehängt - Tests für Versionsnummer und "Schritt läuft nur einmal" Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
79cbaab092
commit
d28e6b2e4b
4 files changed
+214
-154
No files matched your search
@@ -215,7 +215,8 @@ Quellen und Lizenzen: `docs/CREDITS.md`.
|
|||||||
|
|
||||||
Alles liegt im Ordner `data/`: die Datenbank `p42.db` (Konten, Projekte, Links,
|
Alles liegt im Ordner `data/`: die Datenbank `p42.db` (Konten, Projekte, Links,
|
||||||
Monitoring, Vorrat, Reisen, Kanban) und die Bilder in `uploads/`. Neue Tabellen und
|
Monitoring, Vorrat, Reisen, Kanban) und die Bilder in `uploads/`. Neue Tabellen und
|
||||||
Spalten legt der Server beim Start selbst an (Migrationen in `backend/app/db.py`). Für ein Backup den Server kurz stoppen und
|
Spalten legt der Server beim Start selbst an: nummerierte Umstellungen in `backend/app/migrations.py`,
|
||||||
|
der erreichte Stand steht in der Datenbank (`PRAGMA user_version`). Für ein Backup den Server kurz stoppen und
|
||||||
den Ordner `data/` sichern. Ältere `projects.json`/`links.json` werden beim ersten Start
|
den Ordner `data/` sichern. Ältere `projects.json`/`links.json` werden beim ersten Start
|
||||||
automatisch übernommen und danach in `*.importiert` umbenannt.
|
automatisch übernommen und danach in `*.importiert` umbenannt.
|
||||||
|
|
||||||
@@ -232,7 +233,8 @@ backend/
|
|||||||
requirements.txt
|
requirements.txt
|
||||||
app/main.py FastAPI: API-Routen, Uploads, Auslieferung des Frontends
|
app/main.py FastAPI: API-Routen, Uploads, Auslieferung des Frontends
|
||||||
app/config.py liest config.json
|
app/config.py liest config.json
|
||||||
app/db.py SQLite-Schema, Übernahme alter JSON-Daten
|
app/db.py SQLite-Schema (neuester Stand), Verbindungen, Übernahme alter JSON-Daten
|
||||||
|
app/migrations.py Nummerierte Umstellungen bestehender Datenbanken (Regel steht oben in der Datei)
|
||||||
app/auth.py Konten, Anmeldung, Benutzerverwaltung
|
app/auth.py Konten, Anmeldung, Benutzerverwaltung
|
||||||
app/projects.py Projekte/Artikel, Bild-Uploads, HTML-Bereinigung
|
app/projects.py Projekte/Artikel, Bild-Uploads, HTML-Bereinigung
|
||||||
app/links.py Schnellzugriff-Links pro Konto
|
app/links.py Schnellzugriff-Links pro Konto
|
||||||
|
|||||||
+7
-152
@@ -265,163 +265,18 @@ def _import_legacy(conn) -> None:
|
|||||||
links_json.rename(links_json.with_name("links.json.importiert"))
|
links_json.rename(links_json.with_name("links.json.importiert"))
|
||||||
|
|
||||||
|
|
||||||
def _migrate(conn) -> None:
|
|
||||||
"""Vorrat/Einkauf aus früheren Versionen (ohne Einheiten, ohne Listen oder Vorrat ohne Liste) neu aufbauen.
|
|
||||||
|
|
||||||
Alles, was noch keiner Liste gehört, kommt in die erste Liste bzw. in eine neue Liste "Zuhause",
|
|
||||||
die alle Konten sehen.
|
|
||||||
"""
|
|
||||||
info = {r["name"]: r for r in conn.execute("PRAGMA table_info(household_items)")}
|
|
||||||
if "list_id" in info and info["list_id"]["notnull"]:
|
|
||||||
# Neue Datenbank: einmalig eine erste Liste anlegen (wer später alle löscht, bekommt keine zurück)
|
|
||||||
if get_meta(conn, "haushalt_start") is None:
|
|
||||||
if not conn.execute("SELECT 1 FROM shopping_lists").fetchone():
|
|
||||||
conn.execute(
|
|
||||||
"INSERT INTO shopping_lists (name, everyone, position, created_at) VALUES ('Zuhause', 1, 1, ?)", (now_iso(),)
|
|
||||||
)
|
|
||||||
conn.execute("INSERT INTO meta (key, value) VALUES ('haushalt_start', '1')")
|
|
||||||
return
|
|
||||||
row = conn.execute("SELECT id FROM shopping_lists ORDER BY position, id LIMIT 1").fetchone()
|
|
||||||
if row:
|
|
||||||
default = row["id"]
|
|
||||||
# Die Zwischenversion nannte die gemeinsame Liste "Einkaufsliste"; jetzt enthält sie auch den Vorrat
|
|
||||||
conn.execute("UPDATE shopping_lists SET name = 'Zuhause' WHERE id = ? AND name = 'Einkaufsliste' AND created_by IS NULL", (default,))
|
|
||||||
else:
|
|
||||||
default = conn.execute(
|
|
||||||
"INSERT INTO shopping_lists (name, everyone, position, created_at) VALUES ('Zuhause', 1, 1, ?)", (now_iso(),)
|
|
||||||
).lastrowid
|
|
||||||
unit = "unit" if "unit" in info else "'stk'"
|
|
||||||
list_id = "COALESCE(list_id, ?)" if "list_id" in info else "?"
|
|
||||||
conn.execute("DROP INDEX IF EXISTS household_vorrat")
|
|
||||||
conn.execute("DROP INDEX IF EXISTS household_einkauf")
|
|
||||||
conn.execute("ALTER TABLE household_items RENAME TO household_items_alt")
|
|
||||||
conn.execute("CREATE TABLE household_items " + HOUSEHOLD_TABLE)
|
|
||||||
conn.execute(
|
|
||||||
f"""INSERT INTO household_items (id, key, name, list, list_id, quantity, unit, updated_at, updated_by)
|
|
||||||
SELECT id, key, name, list, {list_id}, quantity, {unit}, updated_at, updated_by FROM household_items_alt""",
|
|
||||||
(default,),
|
|
||||||
)
|
|
||||||
conn.execute("DROP TABLE household_items_alt")
|
|
||||||
conn.execute("INSERT OR REPLACE INTO meta (key, value) VALUES ('haushalt_start', '1')")
|
|
||||||
|
|
||||||
|
|
||||||
def _migrate_checked(conn) -> None:
|
|
||||||
# Einkauf ohne Haken-Spalte: ergänzen, bestehende Einträge sind nicht abgehakt
|
|
||||||
columns = {r["name"] for r in conn.execute("PRAGMA table_info(household_items)")}
|
|
||||||
if "checked" not in columns:
|
|
||||||
conn.execute("ALTER TABLE household_items ADD COLUMN checked INTEGER NOT NULL DEFAULT 0")
|
|
||||||
|
|
||||||
|
|
||||||
def _migrate_travel(conn) -> None:
|
|
||||||
# Reise-Städte aus einer Vorversion ohne Höhe/Zeitzone/Hauptstadt: Spalten ergänzen (Hauptstadt füllt reisen.py nach)
|
|
||||||
columns = {r["name"] for r in conn.execute("PRAGMA table_info(travel_cities)")}
|
|
||||||
for column, kind in (("elevation", "REAL"), ("timezone", "TEXT"), ("capital", "INTEGER")):
|
|
||||||
if column not in columns:
|
|
||||||
conn.execute(f"ALTER TABLE travel_cities ADD COLUMN {column} {kind}")
|
|
||||||
|
|
||||||
|
|
||||||
def _migrate_kanban_categories(conn) -> None:
|
|
||||||
"""Kategorien wieder entfernt: kanban_projects ohne category_id neu aufbauen.
|
|
||||||
|
|
||||||
Aufgaben und Artikel-Links verweisen per ON DELETE CASCADE auf kanban_projects. Ein normales
|
|
||||||
DROP würde sie mitlöschen, deshalb das SQLite-Verfahren: Fremdschlüssel aus, neue Tabelle
|
|
||||||
anlegen, kopieren, alte löschen, umbenennen, Verweise prüfen.
|
|
||||||
"""
|
|
||||||
columns = {r["name"] for r in conn.execute("PRAGMA table_info(kanban_projects)")}
|
|
||||||
if "category_id" not in columns:
|
|
||||||
conn.execute("DROP TABLE IF EXISTS kanban_categories")
|
|
||||||
return
|
|
||||||
conn.commit()
|
|
||||||
conn.execute("PRAGMA foreign_keys = OFF") # wirkt nur außerhalb einer Transaktion
|
|
||||||
try:
|
|
||||||
conn.execute("BEGIN")
|
|
||||||
conn.execute("CREATE TABLE kanban_projects_neu " + KANBAN_PROJECTS_TABLE)
|
|
||||||
conn.execute(
|
|
||||||
"""INSERT INTO kanban_projects_neu (id, name, description, position, created_at, created_by, updated_at, updated_by)
|
|
||||||
SELECT id, name, description, position, created_at, created_by, updated_at, updated_by FROM kanban_projects"""
|
|
||||||
)
|
|
||||||
conn.execute("DROP TABLE kanban_projects")
|
|
||||||
conn.execute("ALTER TABLE kanban_projects_neu RENAME TO kanban_projects")
|
|
||||||
conn.execute("DROP TABLE kanban_categories")
|
|
||||||
if conn.execute("PRAGMA foreign_key_check").fetchall():
|
|
||||||
raise RuntimeError("Kanban-Migration: ungültige Verweise, Änderungen werden zurückgenommen.")
|
|
||||||
conn.commit()
|
|
||||||
except Exception:
|
|
||||||
conn.rollback()
|
|
||||||
raise
|
|
||||||
finally:
|
|
||||||
conn.execute("PRAGMA foreign_keys = ON")
|
|
||||||
|
|
||||||
|
|
||||||
def _migrate_kanban_priority(conn) -> None:
|
|
||||||
# Aufgaben ohne Priorität: Spalte ergänzen, bestehende Aufgaben werden "mittel"
|
|
||||||
columns = {r["name"] for r in conn.execute("PRAGMA table_info(kanban_tasks)")}
|
|
||||||
if "priority" not in columns:
|
|
||||||
conn.execute(
|
|
||||||
"ALTER TABLE kanban_tasks ADD COLUMN priority INTEGER NOT NULL DEFAULT 2 CHECK (priority IN (1, 2, 3))"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _migrate_kanban_done(conn) -> None:
|
|
||||||
"""Aufgaben ohne "erledigt seit": Spalte ergänzen. Schon erledigte Aufgaben zählen ab jetzt,
|
|
||||||
damit beim Update nichts sofort archiviert oder gelöscht wird."""
|
|
||||||
columns = {r["name"] for r in conn.execute("PRAGMA table_info(kanban_tasks)")}
|
|
||||||
if "done_at" not in columns:
|
|
||||||
conn.execute("ALTER TABLE kanban_tasks ADD COLUMN done_at TEXT")
|
|
||||||
conn.execute("UPDATE kanban_tasks SET done_at = ? WHERE status = 'erledigt'", (now_iso(),))
|
|
||||||
|
|
||||||
|
|
||||||
def _migrate_monitor_token(conn) -> None:
|
|
||||||
# Monitore aus der Zeit vor P42-S.M.: Spalte für den Token ergänzen
|
|
||||||
columns = {r["name"] for r in conn.execute("PRAGMA table_info(monitors)")}
|
|
||||||
if "token" not in columns:
|
|
||||||
conn.execute("ALTER TABLE monitors ADD COLUMN token TEXT")
|
|
||||||
|
|
||||||
|
|
||||||
def _migrate_kanban_completed(conn) -> None:
|
|
||||||
# Projekte "abgeschlossen": Spalte ergänzen, falls sie fehlt
|
|
||||||
columns = {r["name"] for r in conn.execute("PRAGMA table_info(kanban_projects)")}
|
|
||||||
if "completed_at" not in columns:
|
|
||||||
conn.execute("ALTER TABLE kanban_projects ADD COLUMN completed_at TEXT")
|
|
||||||
|
|
||||||
|
|
||||||
def _migrate_kanban(conn) -> None:
|
|
||||||
# Aufgaben aus der Zeit vor den Projekten: in "Allgemeines Board" übernehmen
|
|
||||||
columns = {r["name"] for r in conn.execute("PRAGMA table_info(kanban_tasks)")}
|
|
||||||
if "project_id" in columns:
|
|
||||||
return
|
|
||||||
conn.execute("ALTER TABLE kanban_tasks RENAME TO kanban_tasks_alt")
|
|
||||||
conn.execute("CREATE TABLE kanban_tasks " + KANBAN_TASKS_TABLE)
|
|
||||||
if conn.execute("SELECT COUNT(*) FROM kanban_tasks_alt").fetchone()[0]:
|
|
||||||
now = now_iso()
|
|
||||||
project = conn.execute(
|
|
||||||
"INSERT INTO kanban_projects (name, position, created_at, updated_at) VALUES ('Allgemeines Board', 1, ?, ?)",
|
|
||||||
(now, now),
|
|
||||||
).lastrowid
|
|
||||||
conn.execute(
|
|
||||||
"""INSERT INTO kanban_tasks (id, project_id, title, description, status, assignee, position,
|
|
||||||
created_at, created_by, updated_at, updated_by)
|
|
||||||
SELECT id, ?, title, description, status, assignee, position, created_at, created_by, updated_at, updated_by
|
|
||||||
FROM kanban_tasks_alt""",
|
|
||||||
(project,),
|
|
||||||
)
|
|
||||||
conn.execute("DROP TABLE kanban_tasks_alt")
|
|
||||||
|
|
||||||
|
|
||||||
def init() -> None:
|
def init() -> None:
|
||||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
with transaction() as conn:
|
with transaction() as conn:
|
||||||
conn.execute("PRAGMA journal_mode = WAL")
|
conn.execute("PRAGMA journal_mode = WAL")
|
||||||
|
new = conn.execute("SELECT COUNT(*) FROM sqlite_master WHERE type = 'table'").fetchone()[0] == 0
|
||||||
conn.executescript(SCHEMA)
|
conn.executescript(SCHEMA)
|
||||||
_migrate(conn)
|
# Erst hier importieren: migrations.py nutzt die Tabellen-Definitionen aus diesem Modul
|
||||||
_migrate_checked(conn)
|
from . import migrations
|
||||||
_migrate_travel(conn)
|
if new:
|
||||||
_migrate_monitor_token(conn)
|
migrations.start(conn)
|
||||||
_migrate_kanban_categories(conn)
|
else:
|
||||||
_migrate_kanban_completed(conn)
|
migrations.upgrade(conn)
|
||||||
_migrate_kanban(conn)
|
|
||||||
_migrate_kanban_priority(conn)
|
|
||||||
_migrate_kanban_done(conn)
|
|
||||||
_import_legacy(conn)
|
_import_legacy(conn)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
"""Datenbank-Umstellungen mit Versionsnummer (PRAGMA user_version).
|
||||||
|
|
||||||
|
SCHEMA in db.py beschreibt immer den neuesten Stand. Bei einer Änderung am Schema:
|
||||||
|
1. SCHEMA in db.py anpassen – neue Datenbanken entstehen direkt so und bekommen die höchste Nummer.
|
||||||
|
2. Unten an STEPS eine Funktion anhängen, die eine bestehende Datenbank vom vorigen Stand dorthin bringt.
|
||||||
|
Jeder Schritt läuft genau einmal, in einer Transaktion mit dem Hochzählen der Nummer.
|
||||||
|
Tests: tests/test_migrationen.py (alte Stände nachbauen und umstellen).
|
||||||
|
"""
|
||||||
|
from .db import HOUSEHOLD_TABLE, KANBAN_PROJECTS_TABLE, KANBAN_TASKS_TABLE, now_iso
|
||||||
|
|
||||||
|
|
||||||
|
def start(conn) -> None:
|
||||||
|
"""Neue, leere Datenbank: erste Liste anlegen und gleich auf den neuesten Stand setzen."""
|
||||||
|
conn.execute("INSERT INTO shopping_lists (name, everyone, position, created_at) VALUES ('Zuhause', 1, 1, ?)", (now_iso(),))
|
||||||
|
conn.execute(f"PRAGMA user_version = {len(STEPS)}")
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade(conn) -> None:
|
||||||
|
"""Bestehende Datenbank: alle Schritte nach ihrer Nummer nacheinander ausführen."""
|
||||||
|
version = conn.execute("PRAGMA user_version").fetchone()[0]
|
||||||
|
for number, step in enumerate(STEPS[version:], start=version + 1):
|
||||||
|
step(conn)
|
||||||
|
conn.execute(f"PRAGMA user_version = {number}")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Schritt 1: Altbestand aus der Zeit vor der Versionsnummer ----------
|
||||||
|
# Diese Umstellungen prüfen noch selbst, ob sie nötig sind – eine Datenbank ohne Nummer kann
|
||||||
|
# auf jedem früheren Stand sein. Reihenfolge wie früher beim Start.
|
||||||
|
|
||||||
|
|
||||||
|
def _v1_altbestand(conn) -> None:
|
||||||
|
_household(conn)
|
||||||
|
_household_checked(conn)
|
||||||
|
_travel_columns(conn)
|
||||||
|
_monitor_token(conn)
|
||||||
|
_kanban_without_categories(conn)
|
||||||
|
_kanban_completed(conn)
|
||||||
|
_kanban_projects(conn)
|
||||||
|
_kanban_priority(conn)
|
||||||
|
_kanban_done_at(conn)
|
||||||
|
|
||||||
|
|
||||||
|
def _household(conn) -> None:
|
||||||
|
"""Vorrat/Einkauf aus früheren Versionen (ohne Einheiten, ohne Listen oder Vorrat ohne Liste) neu aufbauen.
|
||||||
|
|
||||||
|
Alles, was noch keiner Liste gehört, kommt in die erste Liste bzw. in eine neue Liste "Zuhause",
|
||||||
|
die alle Konten sehen.
|
||||||
|
"""
|
||||||
|
info = {r["name"]: r for r in conn.execute("PRAGMA table_info(household_items)")}
|
||||||
|
if "list_id" in info and info["list_id"]["notnull"]:
|
||||||
|
return
|
||||||
|
row = conn.execute("SELECT id FROM shopping_lists ORDER BY position, id LIMIT 1").fetchone()
|
||||||
|
if row:
|
||||||
|
default = row["id"]
|
||||||
|
# Die Zwischenversion nannte die gemeinsame Liste "Einkaufsliste"; jetzt enthält sie auch den Vorrat
|
||||||
|
conn.execute("UPDATE shopping_lists SET name = 'Zuhause' WHERE id = ? AND name = 'Einkaufsliste' AND created_by IS NULL", (default,))
|
||||||
|
else:
|
||||||
|
default = conn.execute(
|
||||||
|
"INSERT INTO shopping_lists (name, everyone, position, created_at) VALUES ('Zuhause', 1, 1, ?)", (now_iso(),)
|
||||||
|
).lastrowid
|
||||||
|
unit = "unit" if "unit" in info else "'stk'"
|
||||||
|
list_id = "COALESCE(list_id, ?)" if "list_id" in info else "?"
|
||||||
|
conn.execute("DROP INDEX IF EXISTS household_vorrat")
|
||||||
|
conn.execute("DROP INDEX IF EXISTS household_einkauf")
|
||||||
|
conn.execute("ALTER TABLE household_items RENAME TO household_items_alt")
|
||||||
|
conn.execute("CREATE TABLE household_items " + HOUSEHOLD_TABLE)
|
||||||
|
conn.execute(
|
||||||
|
f"""INSERT INTO household_items (id, key, name, list, list_id, quantity, unit, updated_at, updated_by)
|
||||||
|
SELECT id, key, name, list, {list_id}, quantity, {unit}, updated_at, updated_by FROM household_items_alt""",
|
||||||
|
(default,),
|
||||||
|
)
|
||||||
|
conn.execute("DROP TABLE household_items_alt")
|
||||||
|
|
||||||
|
|
||||||
|
def _household_checked(conn) -> None:
|
||||||
|
# Einkauf ohne Haken-Spalte: ergänzen, bestehende Einträge sind nicht abgehakt
|
||||||
|
columns = {r["name"] for r in conn.execute("PRAGMA table_info(household_items)")}
|
||||||
|
if "checked" not in columns:
|
||||||
|
conn.execute("ALTER TABLE household_items ADD COLUMN checked INTEGER NOT NULL DEFAULT 0")
|
||||||
|
|
||||||
|
|
||||||
|
def _travel_columns(conn) -> None:
|
||||||
|
# Reise-Städte aus einer Vorversion ohne Höhe/Zeitzone/Hauptstadt: Spalten ergänzen (Hauptstadt füllt reisen.py nach)
|
||||||
|
columns = {r["name"] for r in conn.execute("PRAGMA table_info(travel_cities)")}
|
||||||
|
for column, kind in (("elevation", "REAL"), ("timezone", "TEXT"), ("capital", "INTEGER")):
|
||||||
|
if column not in columns:
|
||||||
|
conn.execute(f"ALTER TABLE travel_cities ADD COLUMN {column} {kind}")
|
||||||
|
|
||||||
|
|
||||||
|
def _kanban_without_categories(conn) -> None:
|
||||||
|
"""Kategorien wieder entfernt: kanban_projects ohne category_id neu aufbauen.
|
||||||
|
|
||||||
|
Aufgaben und Artikel-Links verweisen per ON DELETE CASCADE auf kanban_projects. Ein normales
|
||||||
|
DROP würde sie mitlöschen, deshalb das SQLite-Verfahren: Fremdschlüssel aus, neue Tabelle
|
||||||
|
anlegen, kopieren, alte löschen, umbenennen, Verweise prüfen.
|
||||||
|
"""
|
||||||
|
columns = {r["name"] for r in conn.execute("PRAGMA table_info(kanban_projects)")}
|
||||||
|
if "category_id" not in columns:
|
||||||
|
conn.execute("DROP TABLE IF EXISTS kanban_categories")
|
||||||
|
return
|
||||||
|
conn.commit()
|
||||||
|
conn.execute("PRAGMA foreign_keys = OFF") # wirkt nur außerhalb einer Transaktion
|
||||||
|
try:
|
||||||
|
conn.execute("BEGIN")
|
||||||
|
conn.execute("CREATE TABLE kanban_projects_neu " + KANBAN_PROJECTS_TABLE)
|
||||||
|
conn.execute(
|
||||||
|
"""INSERT INTO kanban_projects_neu (id, name, description, position, created_at, created_by, updated_at, updated_by)
|
||||||
|
SELECT id, name, description, position, created_at, created_by, updated_at, updated_by FROM kanban_projects"""
|
||||||
|
)
|
||||||
|
conn.execute("DROP TABLE kanban_projects")
|
||||||
|
conn.execute("ALTER TABLE kanban_projects_neu RENAME TO kanban_projects")
|
||||||
|
conn.execute("DROP TABLE kanban_categories")
|
||||||
|
if conn.execute("PRAGMA foreign_key_check").fetchall():
|
||||||
|
raise RuntimeError("Kanban-Migration: ungültige Verweise, Änderungen werden zurückgenommen.")
|
||||||
|
conn.commit()
|
||||||
|
except Exception:
|
||||||
|
conn.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
conn.execute("PRAGMA foreign_keys = ON")
|
||||||
|
|
||||||
|
|
||||||
|
def _kanban_priority(conn) -> None:
|
||||||
|
# Aufgaben ohne Priorität: Spalte ergänzen, bestehende Aufgaben werden "mittel"
|
||||||
|
columns = {r["name"] for r in conn.execute("PRAGMA table_info(kanban_tasks)")}
|
||||||
|
if "priority" not in columns:
|
||||||
|
conn.execute(
|
||||||
|
"ALTER TABLE kanban_tasks ADD COLUMN priority INTEGER NOT NULL DEFAULT 2 CHECK (priority IN (1, 2, 3))"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _kanban_done_at(conn) -> None:
|
||||||
|
"""Aufgaben ohne "erledigt seit": Spalte ergänzen. Schon erledigte Aufgaben zählen ab jetzt,
|
||||||
|
damit beim Update nichts sofort archiviert oder gelöscht wird."""
|
||||||
|
columns = {r["name"] for r in conn.execute("PRAGMA table_info(kanban_tasks)")}
|
||||||
|
if "done_at" not in columns:
|
||||||
|
conn.execute("ALTER TABLE kanban_tasks ADD COLUMN done_at TEXT")
|
||||||
|
conn.execute("UPDATE kanban_tasks SET done_at = ? WHERE status = 'erledigt'", (now_iso(),))
|
||||||
|
|
||||||
|
|
||||||
|
def _monitor_token(conn) -> None:
|
||||||
|
# Monitore aus der Zeit vor P42-S.M.: Spalte für den Token ergänzen
|
||||||
|
columns = {r["name"] for r in conn.execute("PRAGMA table_info(monitors)")}
|
||||||
|
if "token" not in columns:
|
||||||
|
conn.execute("ALTER TABLE monitors ADD COLUMN token TEXT")
|
||||||
|
|
||||||
|
|
||||||
|
def _kanban_completed(conn) -> None:
|
||||||
|
# Projekte "abgeschlossen": Spalte ergänzen, falls sie fehlt
|
||||||
|
columns = {r["name"] for r in conn.execute("PRAGMA table_info(kanban_projects)")}
|
||||||
|
if "completed_at" not in columns:
|
||||||
|
conn.execute("ALTER TABLE kanban_projects ADD COLUMN completed_at TEXT")
|
||||||
|
|
||||||
|
|
||||||
|
def _kanban_projects(conn) -> None:
|
||||||
|
# Aufgaben aus der Zeit vor den Projekten: in "Allgemeines Board" übernehmen
|
||||||
|
columns = {r["name"] for r in conn.execute("PRAGMA table_info(kanban_tasks)")}
|
||||||
|
if "project_id" in columns:
|
||||||
|
return
|
||||||
|
conn.execute("ALTER TABLE kanban_tasks RENAME TO kanban_tasks_alt")
|
||||||
|
conn.execute("CREATE TABLE kanban_tasks " + KANBAN_TASKS_TABLE)
|
||||||
|
if conn.execute("SELECT COUNT(*) FROM kanban_tasks_alt").fetchone()[0]:
|
||||||
|
now = now_iso()
|
||||||
|
project = conn.execute(
|
||||||
|
"INSERT INTO kanban_projects (name, position, created_at, updated_at) VALUES ('Allgemeines Board', 1, ?, ?)",
|
||||||
|
(now, now),
|
||||||
|
).lastrowid
|
||||||
|
conn.execute(
|
||||||
|
"""INSERT INTO kanban_tasks (id, project_id, title, description, status, assignee, position,
|
||||||
|
created_at, created_by, updated_at, updated_by)
|
||||||
|
SELECT id, ?, title, description, status, assignee, position, created_at, created_by, updated_at, updated_by
|
||||||
|
FROM kanban_tasks_alt""",
|
||||||
|
(project,),
|
||||||
|
)
|
||||||
|
conn.execute("DROP TABLE kanban_tasks_alt")
|
||||||
|
|
||||||
|
|
||||||
|
# Reihenfolge = Versionsnummer (Index + 1). Nur anhängen, nie umsortieren oder entfernen.
|
||||||
|
STEPS = [
|
||||||
|
_v1_altbestand,
|
||||||
|
]
|
||||||
@@ -136,3 +136,25 @@ def test_geloeschte_listen_kommen_nicht_zurueck(conn):
|
|||||||
conn.execute("DELETE FROM shopping_lists")
|
conn.execute("DELETE FROM shopping_lists")
|
||||||
db.init()
|
db.init()
|
||||||
assert conn.execute("SELECT COUNT(*) FROM shopping_lists").fetchone()[0] == 0
|
assert conn.execute("SELECT COUNT(*) FROM shopping_lists").fetchone()[0] == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_neue_datenbank_hat_neueste_version(conn):
|
||||||
|
from app import migrations
|
||||||
|
assert conn.execute("PRAGMA user_version").fetchone()[0] == len(migrations.STEPS)
|
||||||
|
|
||||||
|
|
||||||
|
def test_alte_datenbank_bekommt_neueste_version(old_db):
|
||||||
|
from app import migrations
|
||||||
|
conn = old_db(HAUSHALT_OHNE_LISTEN)
|
||||||
|
assert conn.execute("PRAGMA user_version").fetchone()[0] == len(migrations.STEPS)
|
||||||
|
|
||||||
|
|
||||||
|
def test_schritt_laeuft_nur_einmal(old_db, monkeypatch):
|
||||||
|
from app import migrations
|
||||||
|
calls = []
|
||||||
|
conn = old_db(HAUSHALT_OHNE_LISTEN)
|
||||||
|
monkeypatch.setattr(migrations, "STEPS", [*migrations.STEPS, lambda c: calls.append(1)])
|
||||||
|
db.init()
|
||||||
|
db.init()
|
||||||
|
assert calls == [1]
|
||||||
|
assert conn.execute("PRAGMA user_version").fetchone()[0] == len(migrations.STEPS)
|
||||||
Reference in new issue
Block a user