fix(crm): DB-Migrationen idempotent machen für Neuinstallationen

Bei Neuinstallation erzeugt create_all bereits das gesamte Schema,
so dass nachfolgende ALTER-Table-Migrationen fehlschlagen ("duplicate
column name"). V01_04 prüft nun die Spalten-Existenz per PRAGMA
table_info. checkupdates läuft wieder sequentiell ohne Überspringen.
Konvention in AGENTS.md dokumentiert.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
gg committed 2026-09-15 17:36:52 +02:00
1 parent efef212544
commit 44c411e5d7
2 files changed
+22 -3

No files matched your search

+5
View File
@@ -74,3 +74,8 @@ Bei neuen projektbezogenen Entitäten muss:
## Generierte Dateien nicht beachten
Das Verzeichnis `Client/tsout/` enthält die JavaScript-Ausgabe der TypeScript-Kompilation und wird durch den Build-Prozess (tsc + rollup) erzeugt. Diese Dateien sind nicht manuell zu bearbeiten und brauchen bei der Codeanalyse nicht berücksichtigt zu werden. Ebenso sind die JavaScript-Dateien in `NGINX_Server/html/static/` (z.B. `kbp_crm_00.00.01.js`) die Ausgabe des Rollup-Builds und nicht zu untersuchen.
## Vorgaben für DB-Migrationen (checkdb.py)
Die DB-Versionierung erfolgt über `_db_Vxx_xx`-Funktionen in `auth/model/checkdb.py` und `crm/model/checkdb.py`. Beim Start wird `checkupdates()` aufgerufen und führt alle Funktionen aus, deren Versionsnummer größer ist als die in der Systeminfo-Tabelle vermerkte. Bei einer Neuinstallation (leere Datenbank) erzeugt `_db_V01_00` über `create_all` bereits das **gesamte aktuelle Schema** aus den Models — inklusive aller Spalten, Tabellen und Indizes, die in späteren Migrationen erst nachträglich ergänzt wurden. Die inkrementellen Migrationen laufen danach dennoch sequentiell nach.
Daher gilt die Konvention: **Jede `_db_Vxx_xx`-Migration muss idempotent sein** — sie muss fehlerfrei durchlaufen, auch wenn das von ihr geänderte Schema-Element bereits durch `create_all` existiert. Insbesondere müssen `ALTER TABLE ADD COLUMN`-Anweisungen vorab prüfen, ob die Spalte bereits existiert (z.B. via `PRAGMA table_info`). `CREATE INDEX` verwendet `IF NOT EXISTS`. Daten-UPDATEs auf leeren Tabellen matchen nichts und sind somit unbedenklich. Migrationen, die Seed-Daten einfügen, müssen vorab prüfen, ob diese bereits vorhanden sind.
+17 -3
View File
@@ -83,10 +83,19 @@ def _db_V01_04():
Fügt die Spalte 'position' zur Tabelle 'kontaktadressen' hinzu.
Bei bestehenden Datenbanken, die mit Version 01.03 initialisiert wurden,
fehlt diese Spalte, da das Feld erst nachträglich ergänzt wurde.
Idempotent: Bei Neuinstallation hat create_all die Spalte bereits
angelegt — in diesem Fall wird das ALTER TABLE übersprungen.
"""
from sqlalchemy import text
session.execute(text("ALTER TABLE kontaktadressen ADD COLUMN position VARCHAR(200)"))
# Prüfen, ob die Spalte bereits existiert (Neuinstallation via create_all)
result = session.execute(text("PRAGMA table_info(kontaktadressen)"))
spalten = {row[1] for row in result}
if 'position' not in spalten:
session.execute(text(
"ALTER TABLE kontaktadressen ADD COLUMN position VARCHAR(200)"
))
_updatesysteminfo('01.04')
session.commit()
@@ -217,7 +226,14 @@ def checkupdates():
Ermittelt die aktuelle DB-Version aus der Systeminfo-Tabelle und führt alle
_db_Vxx_xx-Funktionen aus, deren Versionsnummer größer ist. Ist die
Datenbank noch leer (Tabelle fehlt), wird die Ersteinrichtung ausgeführt.
Bei einer Neuinstallation erzeugt _db_V01_00 über create_all bereits alle
Tabellen mit dem aktuellen Schema (inklusive aller später ergänzten Spalten,
Tabellen und Indizes). Die inkrementellen Migrationen laufen trotzdem
sequentiell nach — jede einzelne muss idempotent sein, damit sie auf der
frisch erzeugten DB fehlerfrei durchläuft (siehe Konvention in AGENTS.md).
"""
versionen = sorted(filter(lambda f: f.startswith("_db_V"), globals()))
curr_dbversion = '_db_V01_00'
try:
systeminfo: CrmSysteminfo = CrmSysteminfo.query.get(1)
@@ -225,8 +241,6 @@ def checkupdates():
except (OperationalError, AttributeError):
_db_V01_00()
versionen = sorted(filter(lambda f: f.startswith("_db_V"), globals()))
for v in versionen:
if v > curr_dbversion:
globals()[v]()