ProofedMesh/docs/adr/0004-db-migrations.md

83 lines
6.1 KiB
Markdown
Raw Normal View History

# ADR-0004: Datenbank-Migrationsframework
- **Status:** accepted
- **Datum:** 2026-10-25
- **Entscheider:** CTO
- **Betroffene Dokumente:** `docs/architecture/v1.1.md` Abschnitt 6.2, `db/README.md`, `services/admin-api/README.md`
## Kontext und Problemstellung
ProofedMesh wird mehrere PostgreSQL-Schemas betreiben: `trust-postgres` (Agent Registry, Sink Registry, Data Source Registry, Decisions, `per_diem_rates`, `rate_change`-Log, …) und `expense-postgres` (DB-App als Senke/Datenquelle). Beide müssen über Monate und Releases hinweg schema-sauber versioniert evolvieren — mit nachvollziehbarer Migrationshistorie, deterministischem Rollback und einem CI-Hook, der Breaking Changes vor dem Merge erwischt.
## Entscheidungsoptionen
- **Option A — [golang-migrate](https://github.com/golang-migrate/migrate):** reine SQL-Migrations-Dateien (`NNNN_name.up.sql` + `NNNN_name.down.sql`), CLI + Go-Library, läuft sowohl als init-container als auch programmgesteuert im App-Startup, keine Build-time-Codegen.
- **Option B — [atlas](https://atlasgo.io/):** deklarative Zielschema-Definition in HCL, Migrationen werden auto-generiert, Diff/Lint-Features. Mächtiger, aber vendor-lock-in auf Atlas-DSL.
- **Option C — Build-in migrations im Trust-API (eigene Mini-Lösung):** rohe `database/sql`-Statements in Go-Konstanten, einfacher Zähler in `schema_migrations`-Tabelle. Null externe Dependencies.
- **Option D — [sqlc](https://sqlc.dev/) + [goose](https://github.com/pressly/goose):** sqlc für typsichere Queries, goose für Migrations.
## Entscheidung
**Option A — golang-migrate.**
### Begründung
- **Plattform-neutral.** SQL-Migrations sind lesbar für jeden Datenbank-Admin ohne Go-Kenntnisse. Rollout kann als init-container ohne Go-Runtime laufen (`migrate/migrate:v4` Container).
- **Reine SQL-Dateien** sind in Git-Diffs selbsterklärend und erzeugen keine Build-time-Abhängigkeiten in CI.
- **Dual-Modus:** Dieselben Migrationen lassen sich manuell via CLI gegen die Prod-DB fahren (für kontrollierte Rollouts) oder beim App-Start automatisch anwenden (für Dev/Test). Keine zusätzlichen Werkzeuge nötig.
- **Rollback:** jede `up`-Migration hat ein `down`-Pendant. Nicht alle Rollbacks sind sauber lösbar (Datenverlust bei DROP COLUMN), aber die Konvention zwingt uns, bewusst darüber nachzudenken.
- Atlas (Option B) ist mächtiger, bringt aber eine zusätzliche DSL und einen Vendor-Lock-in — Overkill für unseren jetzigen Scope.
- Goose/sqlc (Option D) ist eine valide Alternative; wenn wir sqlc wegen typsicherer Queries nachrüsten, bleibt golang-migrate trotzdem der sinnvolle Partner. Sqlc ersetzt es nicht.
- Eigenbau (Option C) hat heute wenig Vorteile und morgen viel Wartung.
## Konventionen
- **Pfad:** `db/<schema-name>/migrations/NNNN_description.{up,down}.sql`. Erster Schema-Pfad: `db/trust/migrations/`. Später z.B. `db/expense/migrations/`.
- **Dateinamen:** vierstellige Nummer, `_`, Snake-Case-Beschreibung, `.up.sql` / `.down.sql`. Beispiel: `0001_per_diem_rates.up.sql`.
- **Migrationstabelle:** `schema_migrations` (default von golang-migrate), pro Datenbank.
- **Nummerierung ist monoton und niemals umgenummeriert.** Eine fehlerhafte Migration wird durch eine neue Migration korrigiert, nicht durch Rewrite der History.
- **Immutable Datenfelder:** wo fachlich nötig, erzwingen Trigger im Schema die Unveränderbarkeit (siehe `per_diem_rates` → `BEFORE UPDATE` Trigger, der nur `valid_to` und `rate_change_attestation_ref` ändern lässt).
- **Benötigte Postgres-Extensions** (`btree_gist`, `pgcrypto`, …) werden am Anfang der jeweiligen `up.sql` per `CREATE EXTENSION IF NOT EXISTS …` eingezogen; nicht in einer separaten "setup"-Datei.
## Ausführungsmodell
- **Dev/Test:** Trust-API führt beim Startup `migrate.Up()` aus (Idempotenz garantiert durch `schema_migrations`-Tabelle). Das ist schnell und fehlerresistent für die interne Entwicklung.
- **Prod:** Rollout via dediziertem Kubernetes-Job (`job-migrate-trust`), der VOR dem Rolling-Update der trust-api-Pods läuft. Trust-API startet nur, wenn `schema_migrations.dirty = false` und die höchste Version mindestens die von der neuen Code-Version erwartete ist. Verhindert, dass neuer Code gegen altes Schema läuft.
- **Rollback-Prod:** separater Job `job-migrate-trust-down`, manuell getriggert. CTO-Freigabe als Prozess, nicht als Code-Schranke.
## CI-Hook
Eine neue CI-Job-Gruppe `db` prüft:
1. **Lint:** `sqlfluff` oder `pg_format` auf alle `.sql` (Formatierung + Basis-Syntax).
2. **Dry-run:** eine wegwerfbare Postgres-Instanz im CI-Job hochziehen, `migrate up` ausführen, `migrate down` ausführen, `migrate up` erneut — garantiert, dass Up/Down-Paare konsistent sind.
3. **Keine History-Rewrites:** CI prüft, dass vorhandene Migrations-Dateien in einem PR nicht geändert werden (nur neue Nummern hinzukommen).
Konkrete Implementierung dieses CI-Jobs folgt separat — in diesem Heartbeat landet die Framework-Entscheidung + die erste Migration. Der CI-Job kommt parallel zum Go-CI-Job, den wir ohnehin brauchen.
## Konsequenzen
### Positive
- SQL-first, lesbar für Nicht-Go-Entwickler.
- Dual-Modus (CLI + Library) deckt Dev- und Prod-Workflows mit demselben Werkzeug.
- Keine zusätzliche DSL, keine Codegen-Pipeline.
- Rollback-Pendant pro Up-Migration zwingt zur Reversibilitätsüberlegung.
### Negative / Trade-offs
- Keine automatische Diff-Erzeugung (im Gegensatz zu Atlas) — Migrationen schreibt man von Hand.
- Keine Typsicherheit zwischen Code und Schema — mit sqlc nachrüstbar, wenn der Bedarf entsteht.
- Für sehr große Teams könnte Atlas langfristig besser skalieren — rechtfertigt derzeit die Umstellung nicht.
### Folgearbeiten / Open Points
- CI-Job `db` (`sqlfluff` + Up/Down-Dry-Run) als eigener Commit.
- Operations-Dokumentation für Prod-Rollouts (Runbook `docs/ops/db-migration.md`), sobald der erste Rollout ansteht.
- Entscheidung, ob wir Immutability-Trigger per Default im Repo versionieren oder zusätzliche Row-Level-Security einziehen. Im `per_diem_rates`-Fall klar: Triggers (siehe Migration 0001).
## Links
- `db/README.md`
- `db/trust/migrations/0001_per_diem_rates.{up,down}.sql`
- https://github.com/golang-migrate/migrate