Zum Inhalt

ADR-009: Versioniertes Datenbank-Migrations-Framework

Status: Akzeptiert Datum: 2026-07-04 Entscheider: Kamerplanter Development Team

Kontext

Datenbank-Seeds und einmalige Datenmigrationen wurden bisher als lose Sammlung von Funktionen in app/migrations/ geführt und einzeln, hartkodiert und ungeordnet beim Application-Startup (app/main.py) aufgerufen — rund 18 Aufrufe hintereinander, die bei jedem Start erneut liefen. Dabei fehlten:

  • Tracking, welche Migration auf einer Datenbank bereits ausgeführt wurde
  • Fehler-Isolation — eine einzelne fehlschlagende Migration riss den gesamten Startup mit
  • Deklarierte Reihenfolge — die Ausführungsreihenfolge steckte implizit in der Aufrufsequenz
  • Einheitliche Konventionen für Report-Format und --dry-run
  • Ein Versionierungs-/Rollback-Konzept, das anzeigt, welcher Datenbankstand gerade vorliegt

Diese Lücken führten am 2026-07-04 zu zwei aufeinanderfolgenden Backend-Crashes beim Startup: Einmal an einem seit Issue #306 abgeschafften Enum-Wert (harvest), für den keine Daten-Migration existierte, danach an einem blockierten Home-Assistant-Endpoint. In beiden Fällen riss ein einzelner Schritt den kompletten Startup mit.

Randbedingungen: ArangoDB ist als primäre Datenbank schemalos und AQL-basiert (ADR-001) — ein Migrations-Framework im Alembic-/SQL-Stil passt hier nicht. Das bestehende Muster „idempotente YAML-Seed-Jobs bei jedem Startup" (docs/adr/005, YAML-basierte Seed-Jobs beim Startup) bleibt gültig und wird von einmaligen Migrationen sauber abgegrenzt, nicht ersetzt. Mehrere Backend-Replicas in Produktion dürfen keine doppelte oder konkurrierende Migrationsausführung auslösen.

Entscheidung

Wir führen ein versioniertes, getracktes Migrations-Framework ein und trennen begrifflich klar zwischen Migrationen (einmalig, versioniert) und Seeds (idempotent, immer laufend):

Migration Seed
Zweck Einmalige Transformation bestehender Daten/Struktur Laden/Upsert von Referenzdaten
Ausführung Genau einmal pro Datenbank (getrackt) Bei jedem Startup (idempotent)
Fehler-Policy Fatal — Startup bricht ab Non-fatal — Startup läuft weiter

Kernbausteine:

  • Tracking-Collection schema_migrations — pro angewandter Version ein Dokument mit Versionsnummer, Name, Checksumme des up()-Quelltexts, Zeitstempel und Dauer. Weicht die Checksumme einer bereits angewandten Migration ab, wird das als Warnung geloggt — angewandte Migrationen gelten als unveränderlich, Korrekturen erfolgen als neue Migration.
  • Concurrency-Lock — verhindert, dass mehrere gleichzeitig startende Backend-Replicas dieselbe Migration doppelt ausführen. Derselbe Lock serialisiert auch die Seed-Registry: Ein Replica seedet, die anderen warten. Hat währenddessen ein anderes Replica einen vollständigen Lauf derselben Seed-Eingaben abgeschlossen, seedet das wartende Replica nicht erneut. Läuft die begrenzte Wartezeit ab, bricht der Start ab, statt die Seeds still zu überspringen.
  • Strikt lineare Versionshistorie — Migrationen sind fortlaufend nummeriert (v0001, v0002, …); Lücken vor dem aktuellen Stand sind ein Fehler, Out-of-Order-Anwendung ist ausgeschlossen.
  • Migration-Protokoll mit up()/down(): Rollback wird unterstützt, wo sinnvoll; nicht umkehrbare Datentransformationen deklarieren das ehrlich (reversible = False) statt eine scheinbare Umkehr vorzutäuschen.
  • Baseline-Migration v0001 markiert den Datenbankstand vor Einführung des Frameworks; die fünf bestehenden Migrationen wurden als v0002–v0006 gekapselt.
  • Startup-Reihenfolge: Erst alle ausstehenden Migrationen (fatal bei Fehler), danach die Seed-Registry (isoliert, non-fatal für Referenzdaten).

Datenbank-Migrationen — CLI-Referenz

Für kontrollierte Betriebs-Eingriffe und zum Anlegen neuer Migrationen existiert ein CLI-Einstiegspunkt:

python -m app.migrations upgrade      # alle ausstehenden Migrationen anwenden
python -m app.migrations downgrade    # auf eine Zielversion zurückrollen (nur reversible Migrationen)
python -m app.migrations current      # höchste angewandte Version anzeigen
python -m app.migrations history      # alle angewandten Migrationen mit Zeitstempel
python -m app.migrations create <slug> # neue vNNNN_<slug>.py aus Vorlage erzeugen

upgrade und downgrade unterstützen --dry-run, um den Plan zu berechnen, ohne zu schreiben — das ist Voraussetzung für kontrollierte Produktions-Ausführung. Der CLI-Befehl ändert nichts an der öffentlichen HTTP-API; er ist ein reines Ops-/Entwickler-Werkzeug.

Aktuell laufen Migrationen weiterhin im FastAPI-lifespan beim Start (abgesichert durch den Concurrency-Lock). Mittelfristiges Zielbild ist, sie stattdessen als dediziertes Kubernetes-Job / Helm-pre-upgrade-Hook auszuführen, sodass App-Replicas selbst nie migrieren — der CLI-Entrypoint ist dafür bereits die Grundlage. Stand 2026-10-10 (Issue #2125): Die Betreiberentscheidung lautet, Migrationen und Seeds im lifespan zu belassen; statt eines Jobs gibt ein startupProbe dem Start bis zu 900 s, dokumentiert unter Startdauer des Backends.

Abgelehnte Alternativen

Option Warum verworfen
Leichtgewichtige Registry ohne Versionierung Erfüllt den Wunsch nach rollback-fähiger, reproduzierbarer Historie nicht
Reine Konventionen/Doku ohne Framework-Code Löst die realen Startup-Crashes (fehlende Fehler-Isolation, kein Tracking) nicht
Bestehendes SQL-Tool (Alembic/yoyo) An SQLAlchemy/SQL-DDL gebunden — passt nicht zum schemalosen, AQL-basierten ArangoDB
Migrationen ausschließlich als externer Job, nie im Startup Als Zielbild übernommen, aber kein Sofort-Bruch — der Startup-Pfad mit Lock bleibt interim erhalten

Konsequenzen

Positiv

  • Ein fehlschlagender Referenzdaten-Seed reißt den Startup nicht mehr mit.
  • Einmalige Migrationen laufen genau einmal, getrackt und in definierter Reihenfolge.
  • Der Datenbankstand ist über current/history jederzeit inspizierbar.
  • Replica-Races beim parallelen Pod-Start sind durch den Lock ausgeschlossen — für Migrationen und Seeds.
  • Neue Migrationen folgen einem einheitlichen, getesteten Muster (create <slug>-Scaffolding).

Negativ

  • Zusätzliche Framework-Komplexität und eine neue Collection schema_migrations.
  • Migrations-Autoren müssen Versionierungs- und Idempotenz-Regeln einhalten.
  • Die meisten Datentransformationen bleiben faktisch irreversibel — Rollback ist kein Allheilmittel, sondern eine ehrlich deklarierte Ausnahme.

Folgemaßnahmen

  • Migrationen mittelfristig in ein dediziertes Kubernetes-Job/Helm-Hook auslagern, statt im App-Pod-Lifespan zu laufen.
  • Bei jeder Entfernung/Umbenennung eines persistierten Enum-Werts oder Felds ist in derselben Änderung verpflichtend eine Daten-Migration mitzuliefern (direkte Lehre aus Issue #306).

Verweise

  • Canonical-Entscheidung: spec/decisions/ADR-005-versioned-migration-framework.md (Hinweis: eigene Nummerierung im spec/decisions/-Verzeichnis, unabhängig von der hiesigen docs/adr/-Zählung — dieses Dokument ist die publizierte Fassung von Canonical-ADR-005 unter der nächsten freien Nummer 009)
  • Normative Anforderung: spec/nfr/NFR-016_Datenbank-Migrationsstrategie.md
  • ADR-001 — ArangoDB als schemalose Primär-Datenbank
  • docs/adr/005 — YAML-basierte Seed-Jobs beim Startup (abgegrenztes Seed-Muster)
  • Issue #306 — Abschaffung der harvest-Phase (auslösender Incident)