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 ## 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. 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. Fügt die Spalte 'position' zur Tabelle 'kontaktadressen' hinzu.
Bei bestehenden Datenbanken, die mit Version 01.03 initialisiert wurden, Bei bestehenden Datenbanken, die mit Version 01.03 initialisiert wurden,
fehlt diese Spalte, da das Feld erst nachträglich ergänzt wurde. 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 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') _updatesysteminfo('01.04')
session.commit() session.commit()
@@ -217,7 +226,14 @@ def checkupdates():
Ermittelt die aktuelle DB-Version aus der Systeminfo-Tabelle und führt alle 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 _db_Vxx_xx-Funktionen aus, deren Versionsnummer größer ist. Ist die
Datenbank noch leer (Tabelle fehlt), wird die Ersteinrichtung ausgeführt. 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' curr_dbversion = '_db_V01_00'
try: try:
systeminfo: CrmSysteminfo = CrmSysteminfo.query.get(1) systeminfo: CrmSysteminfo = CrmSysteminfo.query.get(1)
@@ -225,8 +241,6 @@ def checkupdates():
except (OperationalError, AttributeError): except (OperationalError, AttributeError):
_db_V01_00() _db_V01_00()
versionen = sorted(filter(lambda f: f.startswith("_db_V"), globals()))
for v in versionen: for v in versionen:
if v > curr_dbversion: if v > curr_dbversion:
globals()[v]() globals()[v]()