syncova-backup/migrations/000004_jobs.up.sql
Jerrit Fritzsche 610719c316
Some checks failed
CI / Backend (Go) (push) Failing after 3m7s
CI / Frontend (React/TypeScript) (push) Successful in 37s
CI / Sicherheitsprüfungen (push) Successful in 44s
Syncova Backups V1
Enterprise-Backup-, Recovery-, Verification-, Security- und
Monitoring-Plattform fuer Proxmox VE, Windows, Linux und Dateisysteme.

Der Leitsatz, der fast jede Entscheidung erklaert: Ein Backup gilt erst als
vertrauenswuerdig, wenn Integritaet geprueft und Wiederherstellbarkeit
nachgewiesen wurde. Deshalb steigt ein Wiederherstellungspunkt erst nach einem
tatsaechlich durchgefuehrten Restore-Test auf "recoverable", und Unbekanntes
geht in keine Bewertung als "gut" ein.

Umfang (Phasen 0-23):

- Repository Engine: inhaltsadressierte Bloecke, atomares Commit-Protokoll,
  Katalogaufbau allein aus den Manifesten — ohne Datenbank
- Backup Engine: inhaltsabhaengiges Chunking, Deduplizierung trotz
  Verschluesselung, zstd, AES-256-GCM, Streaming mit Gegendruck
- Agenten fuer Windows und Linux mit Auftragsabholung (Pull-Modell)
- Proxmox-Provider mit beiden Zugriffswegen auf die Sicherungsarchive
- Scheduler, Recovery Engine mit Pruefpunkt, Verification, Unveraenderlichkeit
- Weboberflaeche, Kennzahlen, Meldungen, Berichte, Security Center,
  Ransomware-Heuristik (meldet, handelt nie)
- Disaster Recovery, Haertung, Leistungsmessung, Chaos Testing
- Eingefrorene Vertraege fuer API, Migrationen, Backup-Format und Repository
- Auslieferungspaket fuer linux/amd64, linux/arm64 und windows/amd64

Nicht enthalten und als solches gekennzeichnet: Kapazitaetsprognose, Backup
Copy, Changed Block Tracking bei Proxmox, erweiterte Attribute und ACLs.

Gebaut, aber nie auf echter Hardware gefahren: der Windows-Dienst, die
systemd-Einheit und der verpflichtende Proxmox-Meilenstein — ob eine
wiederhergestellte VM startet, ist ungeprueft. Einzelheiten in CHANGELOG.md
und docs/release-candidate.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 09:10:54 +02:00

487 lines
23 KiB
SQL

-- Sicherungsaufträge, Läufe und Zeitsteuerung (Phase 8).
--
-- Zwei Grundsätze prägen dieses Schema:
--
-- 1. Die Datenbank hält Konfiguration und Verweise, niemals Sicherungsdaten
-- (PROMPT.md §138). Ein Manifest wird über manifest_ref adressiert; sein
-- Inhalt liegt im Repository.
-- 2. Ein Lauf ist kein Zustandsfeld am Auftrag, sondern eine eigene Zeile.
-- Ein Auftrag, dessen letzter Ausgang nur als Spalte am Auftrag stünde,
-- liesse keine Historie zu — und ohne Historie ist nicht feststellbar,
-- ob eine Sicherung regelmäßig gelingt.
-- ---------------------------------------------------------------------------
-- Repositories
-- ---------------------------------------------------------------------------
CREATE TABLE repositories (
-- id ist der öffentliche Bezeichner.
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- name ist die sprechende Bezeichnung.
name TEXT UNIQUE NOT NULL,
-- repository_type benennt die Ablageart, etwa local oder s3.
repository_type TEXT NOT NULL DEFAULT 'local',
-- location ist der Pfad oder die Adresse der Ablage.
location TEXT NOT NULL,
-- repository_uuid ist die im Repository selbst hinterlegte Kennung.
--
-- Sie wird gebraucht, um ein versehentlich vertauschtes Repository zu
-- erkennen: Der Katalog wird bei fremder Kennung neu aufgebaut, statt
-- falsche Einträge weiterzuverwenden.
repository_uuid TEXT,
-- status ist der Betriebszustand.
status TEXT NOT NULL DEFAULT 'active',
-- hardened meldet den gehärteten Modus.
hardened BOOLEAN NOT NULL DEFAULT false,
-- capacity_bytes ist der Gesamtplatz, sofern ermittelbar.
capacity_bytes BIGINT,
-- used_bytes ist der belegte Platz beim letzten Blick.
used_bytes BIGINT,
-- last_checked_at ist der Zeitpunkt der letzten Zustandsprüfung.
last_checked_at TIMESTAMPTZ,
-- created_at ist der Anlagezeitpunkt in UTC.
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-- updated_at ist der Zeitpunkt der letzten Änderung in UTC.
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT repositories_status_valid
CHECK (status IN ('active', 'read_only', 'unavailable', 'maintenance'))
);
-- ---------------------------------------------------------------------------
-- Aufbewahrungsregeln
-- ---------------------------------------------------------------------------
CREATE TABLE retention_policies (
-- id ist der öffentliche Bezeichner.
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- name ist die sprechende Bezeichnung.
name TEXT UNIQUE NOT NULL,
-- rules beschreibt die Regeln als JSON.
--
-- Bewusst als JSON und nicht als Spaltensatz: Die Regeln (täglich,
-- wöchentlich, monatlich, jährlich, GFS) entwickeln sich weiter, und jede
-- Erweiterung wäre sonst eine Migration am Kern des Schemas.
rules JSONB NOT NULL,
-- created_at ist der Anlagezeitpunkt in UTC.
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-- updated_at ist der Zeitpunkt der letzten Änderung in UTC.
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ---------------------------------------------------------------------------
-- Sicherungsaufträge
-- ---------------------------------------------------------------------------
CREATE TABLE backup_jobs (
-- id ist der öffentliche Bezeichner.
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- name ist die eindeutige, sprechende Bezeichnung.
name TEXT UNIQUE NOT NULL,
-- description erläutert den Zweck.
description TEXT,
-- status ist der Zustand des Auftrags.
status TEXT NOT NULL DEFAULT 'active',
-- priority ist die Dringlichkeit.
priority TEXT NOT NULL DEFAULT 'normal',
-- schedule_type benennt die Art des Zeitplans.
schedule_type TEXT NOT NULL,
-- schedule_config beschreibt den Zeitplan als JSON.
schedule_config JSONB NOT NULL DEFAULT '{}'::jsonb,
-- repository_id ist das Ziel-Repository.
repository_id UUID REFERENCES repositories(id) ON DELETE RESTRICT,
-- retention_policy_id ist die Aufbewahrungsregel.
retention_policy_id UUID REFERENCES retention_policies(id) ON DELETE SET NULL,
-- encryption_policy_id ist die Verschlüsselungsregel.
encryption_policy_id UUID,
-- verification_policy_id ist die Prüfregel.
verification_policy_id UUID,
-- notification_policy_id ist die Benachrichtigungsregel.
notification_policy_id UUID,
-- rpo_seconds ist der zulässige Datenverlust in Sekunden.
rpo_seconds BIGINT,
-- rto_seconds ist die zulässige Wiederherstellungsdauer in Sekunden.
rto_seconds BIGINT,
-- bandwidth_limit_bps begrenzt den Durchsatz; NULL bedeutet unbegrenzt.
bandwidth_limit_bps BIGINT,
-- max_concurrency begrenzt gleichzeitige Läufe dieses Auftrags.
max_concurrency INTEGER NOT NULL DEFAULT 1,
-- retry_policy beschreibt das Wiederholungsverhalten als JSON.
retry_policy JSONB NOT NULL DEFAULT '{}'::jsonb,
-- next_run_at ist der berechnete nächste Zeitpunkt in UTC.
--
-- Er wird gespeichert und nicht bei jedem Blick neu berechnet: Der
-- Scheduler fragt im Sekundentakt, welche Aufträge fällig sind. Eine
-- Berechnung über alle Aufträge bei jeder Abfrage wäre bei tausend
-- Aufträgen die teuerste Schleife der Anlage.
next_run_at TIMESTAMPTZ,
-- last_run_at ist der Beginn des letzten Laufs in UTC.
last_run_at TIMESTAMPTZ,
-- last_outcome ist der Ausgang des letzten Laufs.
--
-- Redundant zu backup_job_runs, aber die Übersicht braucht ihn ohne
-- Verbund über eine wachsende Historientabelle.
last_outcome TEXT,
-- paused_at ist der Zeitpunkt einer Aussetzung in UTC.
paused_at TIMESTAMPTZ,
-- paused_by benennt, wer ausgesetzt hat.
paused_by UUID REFERENCES users(id) ON DELETE SET NULL,
-- created_by benennt den Anleger.
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
-- created_at ist der Anlagezeitpunkt in UTC.
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-- updated_at ist der Zeitpunkt der letzten Änderung in UTC.
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-- deleted_at ist der Zeitpunkt einer Löschung in UTC.
--
-- Soft-Delete, weil die Läufe eines gelöschten Auftrags im Audit erhalten
-- bleiben müssen. Ein hart gelöschter Auftrag risse die Historie mit.
deleted_at TIMESTAMPTZ,
CONSTRAINT backup_jobs_status_valid
CHECK (status IN ('active', 'paused', 'disabled', 'error')),
CONSTRAINT backup_jobs_priority_valid
CHECK (priority IN ('critical', 'high', 'normal', 'low')),
CONSTRAINT backup_jobs_schedule_type_valid
CHECK (schedule_type IN ('manual', 'interval', 'hourly', 'daily', 'weekly', 'monthly', 'cron')),
CONSTRAINT backup_jobs_concurrency_positive
CHECK (max_concurrency >= 1),
-- Eine Bandbreitengrenze von 0 wäre keine Grenze, sondern ein Stillstand.
CONSTRAINT backup_jobs_bandwidth_positive
CHECK (bandwidth_limit_bps IS NULL OR bandwidth_limit_bps > 0)
);
-- Der Scheduler fragt ausschließlich nach fälligen, aktiven Aufträgen. Der
-- Teilindex hält genau diese Menge klein — ausgesetzte und gelöschte Aufträge
-- stehen gar nicht erst darin.
CREATE INDEX backup_jobs_due_idx
ON backup_jobs (next_run_at)
WHERE status = 'active' AND deleted_at IS NULL;
CREATE INDEX backup_jobs_status_idx ON backup_jobs (status) WHERE deleted_at IS NULL;
CREATE INDEX backup_jobs_repository_idx ON backup_jobs (repository_id);
-- ---------------------------------------------------------------------------
-- Quellen eines Auftrags
-- ---------------------------------------------------------------------------
CREATE TABLE backup_job_sources (
-- id ist der öffentliche Bezeichner.
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- job_id ist der zugehörige Auftrag.
job_id UUID NOT NULL REFERENCES backup_jobs(id) ON DELETE CASCADE,
-- source_type benennt die Art der Quelle.
source_type TEXT NOT NULL,
-- source_id ist die Kennung der Quelle innerhalb ihrer Art.
--
-- Bewusst Text und nicht UUID: Ein Proxmox-Gast heißt "qemu/100", ein
-- Dateipfad "/daten". Eine UUID erzwänge für jede Quelle einen eigenen
-- Datensatz, auch für einen bloßen Pfad.
source_id TEXT NOT NULL,
-- source_name ist die sprechende Bezeichnung.
source_name TEXT,
-- agent_id ist der ausführende Agent, sofern die Quelle einen braucht.
agent_id UUID REFERENCES agents(id) ON DELETE SET NULL,
-- include_patterns beschränkt die Erfassung.
include_patterns JSONB NOT NULL DEFAULT '[]'::jsonb,
-- exclude_patterns nimmt Pfade aus.
exclude_patterns JSONB NOT NULL DEFAULT '[]'::jsonb,
-- created_at ist der Anlagezeitpunkt in UTC.
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT backup_job_sources_type_valid
CHECK (source_type IN ('filesystem', 'proxmox_vm', 'proxmox_container', 'windows_system', 'linux_system')),
-- Dieselbe Quelle zweimal im selben Auftrag wäre ein Konfigurationsfehler,
-- der die Datenmenge verdoppelte.
CONSTRAINT backup_job_sources_unique UNIQUE (job_id, source_type, source_id)
);
CREATE INDEX backup_job_sources_job_idx ON backup_job_sources (job_id);
CREATE INDEX backup_job_sources_source_idx ON backup_job_sources (source_type, source_id);
-- ---------------------------------------------------------------------------
-- Abhängigkeiten zwischen Aufträgen
-- ---------------------------------------------------------------------------
CREATE TABLE backup_job_dependencies (
-- job_id ist der abhängige Auftrag.
job_id UUID NOT NULL REFERENCES backup_jobs(id) ON DELETE CASCADE,
-- depends_on_job_id ist der vorausgesetzte Auftrag.
depends_on_job_id UUID NOT NULL REFERENCES backup_jobs(id) ON DELETE CASCADE,
-- created_at ist der Anlagezeitpunkt in UTC.
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (job_id, depends_on_job_id),
-- Ein Auftrag, der von sich selbst abhängt, liefe nie an. Längere Ringe
-- muss die Anwendung erkennen — das kann eine Datenbank nicht.
CONSTRAINT backup_job_dependencies_no_self CHECK (job_id <> depends_on_job_id)
);
CREATE INDEX backup_job_dependencies_reverse_idx ON backup_job_dependencies (depends_on_job_id);
-- ---------------------------------------------------------------------------
-- Wartungsfenster
-- ---------------------------------------------------------------------------
CREATE TABLE maintenance_windows (
-- id ist der öffentliche Bezeichner.
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- name ist die sprechende Bezeichnung.
name TEXT NOT NULL,
-- window_kind benennt die Wirkung: sperrend oder erlaubend.
window_kind TEXT NOT NULL DEFAULT 'blackout',
-- starts_at ist der Beginn eines einmaligen Fensters in UTC.
starts_at TIMESTAMPTZ,
-- ends_at ist das Ende eines einmaligen Fensters in UTC.
ends_at TIMESTAMPTZ,
-- recurrence beschreibt ein wiederkehrendes Fenster als JSON.
recurrence JSONB,
-- enabled meldet ein wirksames Fenster.
enabled BOOLEAN NOT NULL DEFAULT true,
-- created_by benennt den Anleger.
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
-- created_at ist der Anlagezeitpunkt in UTC.
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-- updated_at ist der Zeitpunkt der letzten Änderung in UTC.
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT maintenance_windows_kind_valid
CHECK (window_kind IN ('blackout', 'allowed')),
-- Ein Fenster ist entweder einmalig oder wiederkehrend. Beides zugleich
-- wäre mehrdeutig, keines von beidem wirkungslos.
CONSTRAINT maintenance_windows_definition_complete CHECK (
(recurrence IS NOT NULL AND starts_at IS NULL AND ends_at IS NULL)
OR (recurrence IS NULL AND starts_at IS NOT NULL AND ends_at IS NOT NULL AND ends_at > starts_at)
)
);
CREATE INDEX maintenance_windows_enabled_idx ON maintenance_windows (enabled) WHERE enabled;
-- Zuordnung von Fenstern zu einzelnen Aufträgen. Fehlt ein Eintrag, gilt das
-- Fenster für alle Aufträge — das ist der sichere Standard.
CREATE TABLE maintenance_window_jobs (
-- window_id ist das Fenster.
window_id UUID NOT NULL REFERENCES maintenance_windows(id) ON DELETE CASCADE,
-- job_id ist der betroffene Auftrag.
job_id UUID NOT NULL REFERENCES backup_jobs(id) ON DELETE CASCADE,
PRIMARY KEY (window_id, job_id)
);
-- ---------------------------------------------------------------------------
-- Sicherungsketten
-- ---------------------------------------------------------------------------
CREATE TABLE backup_chains (
-- id ist der öffentliche Bezeichner.
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- source_reference benennt die gesicherte Quelle.
source_reference TEXT NOT NULL,
-- repository_id ist das Repository der Kette.
repository_id UUID NOT NULL REFERENCES repositories(id) ON DELETE RESTRICT,
-- status ist der Zustand der Kette.
status TEXT NOT NULL DEFAULT 'active',
-- created_at ist der Anlagezeitpunkt in UTC.
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT backup_chains_status_valid
CHECK (status IN ('active', 'broken', 'closed'))
);
CREATE INDEX backup_chains_source_idx ON backup_chains (source_reference, repository_id);
-- ---------------------------------------------------------------------------
-- Läufe
-- ---------------------------------------------------------------------------
CREATE TABLE backup_job_runs (
-- id ist der öffentliche Bezeichner.
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- job_id ist der ausgeführte Auftrag.
job_id UUID NOT NULL REFERENCES backup_jobs(id) ON DELETE CASCADE,
-- status ist der Zustand des Laufs.
status TEXT NOT NULL DEFAULT 'queued',
-- trigger benennt den Auslöser: Zeitplan, Anwender oder Wiederholung.
trigger TEXT NOT NULL DEFAULT 'schedule',
-- attempt_number ist die Nummer des Versuchs, beginnend bei 1.
attempt_number INTEGER NOT NULL DEFAULT 1,
-- scheduled_for ist der geplante Zeitpunkt in UTC.
--
-- Er steht neben started_at, weil die Abweichung die eigentliche Auskunft
-- ist: Ein Lauf, der regelmäßig eine Stunde zu spät beginnt, hat ein
-- Problem, das man ohne diesen Vergleich nicht sieht.
scheduled_for TIMESTAMPTZ,
-- started_at ist der tatsächliche Beginn in UTC.
started_at TIMESTAMPTZ,
-- completed_at ist das Ende in UTC.
completed_at TIMESTAMPTZ,
-- bytes_processed ist die gelesene Datenmenge.
bytes_processed BIGINT NOT NULL DEFAULT 0,
-- bytes_written ist die abgelegte Datenmenge.
bytes_written BIGINT NOT NULL DEFAULT 0,
-- bytes_transferred ist die über das Netz übertragene Menge.
bytes_transferred BIGINT NOT NULL DEFAULT 0,
-- throughput_bps ist der erreichte Durchsatz.
throughput_bps BIGINT,
-- files_processed ist die Zahl bearbeiteter Objekte.
files_processed BIGINT NOT NULL DEFAULT 0,
-- files_skipped ist die Zahl übergangener Objekte.
--
-- Ist sie größer als 0, ist der Lauf ein Teilfehler und niemals ein
-- Erfolg (PROMPT.md §138).
files_skipped BIGINT NOT NULL DEFAULT 0,
-- error_code ist die Fehlerkennung in SCREAMING_SNAKE_CASE.
error_code TEXT,
-- error_message ist die verständliche Fehlermeldung.
--
-- Sie enthält niemals Geheimnisse (PROMPT.md §141); die Redaktion
-- geschieht vor dem Schreiben.
error_message TEXT,
-- failure_class ordnet den Fehler ein und entscheidet über Wiederholung.
failure_class TEXT,
-- correlation_id verbindet den Lauf mit seinen Protokollzeilen.
correlation_id UUID NOT NULL,
-- triggered_by benennt den auslösenden Anwender bei manuellem Start.
triggered_by UUID REFERENCES users(id) ON DELETE SET NULL,
-- scheduler_instance benennt den Control-Server, der den Lauf hält.
--
-- Nötig, um nach dem Absturz eines Servers dessen verwaiste Läufe zu
-- erkennen: Ein Lauf, der als laufend gilt, dessen Server aber verschwunden
-- ist, blockiert sonst den Auftrag für immer.
scheduler_instance TEXT,
-- heartbeat_at ist die letzte Lebendmeldung des Laufs in UTC.
heartbeat_at TIMESTAMPTZ,
-- created_at ist der Anlagezeitpunkt in UTC.
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT backup_job_runs_status_valid
CHECK (status IN ('queued', 'running', 'succeeded', 'partial_failure', 'failed', 'cancelled')),
CONSTRAINT backup_job_runs_trigger_valid
CHECK (trigger IN ('schedule', 'manual', 'retry', 'dependency')),
CONSTRAINT backup_job_runs_attempt_positive
CHECK (attempt_number >= 1),
-- Ein abgeschlossener Lauf ohne Endzeitpunkt wäre in jeder Auswertung eine
-- Lücke.
CONSTRAINT backup_job_runs_completion_consistent CHECK (
status IN ('queued', 'running') OR completed_at IS NOT NULL
),
-- Ein Lauf mit übergangenen Objekten darf nicht als Erfolg dastehen.
-- Die Regel gehört in die Datenbank, weil sie sonst von jeder Codestelle
-- einzeln eingehalten werden müsste — und eine davon vergisst es.
CONSTRAINT backup_job_runs_skipped_is_not_success CHECK (
files_skipped = 0 OR status <> 'succeeded'
)
);
CREATE INDEX backup_job_runs_job_started_idx ON backup_job_runs (job_id, started_at DESC);
CREATE INDEX backup_job_runs_status_idx ON backup_job_runs (status)
WHERE status IN ('queued', 'running');
CREATE INDEX backup_job_runs_correlation_idx ON backup_job_runs (correlation_id);
-- Ein Auftrag darf nur einen aktiven Lauf haben. Der Teilindex erzwingt das in
-- der Datenbank statt in der Anwendung: Zwei Control-Server, die gleichzeitig
-- denselben fälligen Auftrag sehen, würden ihn sonst beide starten und zwei
-- Sicherungen derselben Quelle erzeugen.
CREATE UNIQUE INDEX backup_job_runs_single_active_idx
ON backup_job_runs (job_id)
WHERE status IN ('queued', 'running');
-- ---------------------------------------------------------------------------
-- Backups
-- ---------------------------------------------------------------------------
CREATE TABLE backups (
-- id ist der öffentliche Bezeichner.
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- job_run_id ist der erzeugende Lauf.
job_run_id UUID REFERENCES backup_job_runs(id) ON DELETE SET NULL,
-- chain_id ist die Kette.
chain_id UUID REFERENCES backup_chains(id) ON DELETE SET NULL,
-- repository_id ist das Repository.
repository_id UUID NOT NULL REFERENCES repositories(id) ON DELETE RESTRICT,
-- parent_backup_id ist das Elternbackup einer Zusatzsicherung.
parent_backup_id UUID REFERENCES backups(id) ON DELETE SET NULL,
-- backup_id_in_repository ist die Kennung innerhalb des Repositorys.
--
-- Sie ist die Brücke zur Ablage: Das Repository kennt keine UUIDs der
-- Datenbank, und es muss ohne sie auskommen (SYNCOVA_ARCHITECTURE.md §10).
backup_id_in_repository TEXT NOT NULL,
-- backup_type ist die Art des Backups.
backup_type TEXT NOT NULL,
-- consistency_level beschreibt die erreichte Konsistenz.
--
-- Sie wird niemals beschönigt: Eine anwendungskonsistente Sicherung ohne
-- Gastdienst gibt es nicht.
consistency_level TEXT,
-- status ist der Zustand des Backups.
status TEXT NOT NULL DEFAULT 'in_progress',
-- manifest_ref verweist auf das Manifest im Repository.
manifest_ref TEXT NOT NULL,
-- manifest_hash ist die Prüfsumme des Manifests.
manifest_hash TEXT,
-- logical_bytes ist die Menge der Ursprungsdaten.
logical_bytes BIGINT,
-- unique_bytes ist die nach Deduplizierung verbleibende Menge.
unique_bytes BIGINT,
-- compressed_bytes ist die Menge nach Kompression.
compressed_bytes BIGINT,
-- encrypted_bytes ist die abgelegte Menge.
encrypted_bytes BIGINT,
-- started_at ist der Beginn in UTC.
started_at TIMESTAMPTZ,
-- completed_at ist das Ende in UTC.
completed_at TIMESTAMPTZ,
-- integrity_status ist das Ergebnis der letzten Integritätsprüfung.
integrity_status TEXT,
-- verified_at ist der Zeitpunkt der letzten Prüfung in UTC.
verified_at TIMESTAMPTZ,
-- immutable_until ist das Ende der Aufbewahrungspflicht in UTC.
--
-- Bis dahin darf kein Löschvorgang das Backup entfernen — auch kein
-- Administrator (PROMPT.md: Unveränderlichkeit).
immutable_until TIMESTAMPTZ,
-- created_at ist der Anlagezeitpunkt in UTC.
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT backups_type_valid
CHECK (backup_type IN ('full', 'incremental', 'differential', 'synthetic_full')),
CONSTRAINT backups_status_valid
CHECK (status IN ('in_progress', 'complete', 'incomplete', 'corrupt', 'deleted')),
CONSTRAINT backups_consistency_valid
CHECK (consistency_level IS NULL OR consistency_level IN
('crash_consistent', 'filesystem_consistent', 'application_consistent')),
-- Eine Zusatzsicherung ohne Elternbackup wäre eine Kette ohne Anfang.
CONSTRAINT backups_incremental_has_parent CHECK (
backup_type <> 'incremental' OR parent_backup_id IS NOT NULL
),
-- Dieselbe Kennung darf im selben Repository nur einmal vorkommen.
CONSTRAINT backups_repository_unique UNIQUE (repository_id, backup_id_in_repository)
);
CREATE INDEX backups_chain_idx ON backups (chain_id, started_at DESC);
CREATE INDEX backups_repository_status_idx ON backups (repository_id, status);
CREATE INDEX backups_job_run_idx ON backups (job_run_id);
-- Für die Aufbewahrung: Welche Backups dürfen noch nicht gelöscht werden?
CREATE INDEX backups_immutable_idx ON backups (immutable_until)
WHERE immutable_until IS NOT NULL;
-- ---------------------------------------------------------------------------
-- Berechtigungen
-- ---------------------------------------------------------------------------
-- jobs.read, jobs.write und jobs.run stammen bereits aus 000002_identity und
-- werden hier NICHT erneut angelegt. Ein zweites INSERT verletzte die
-- Eindeutigkeit des Namens — die Datenbank hat das beim ersten Versuch
-- richtigerweise abgelehnt. Neu ist allein das Wartungsfenster: Es greift in
-- den Betrieb der gesamten Anlage ein und ist deshalb ein eigenes Recht.
INSERT INTO permissions (name, description) VALUES
('maintenance_windows.write', 'Wartungsfenster einrichten und ändern');
-- Ein Wartungsfenster unterdrückt Sicherungen. Wer es setzen darf, kann damit
-- den Schutz der ganzen Anlage aussetzen — das bleibt den Administratoren
-- vorbehalten.
INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id FROM roles r, permissions p
WHERE r.name IN ('super_administrator', 'infrastructure_administrator')
AND p.name = 'maintenance_windows.write';