-- RET-03: Löschworkflow & Aufbewahrungssperre (Legal Hold). -- -- legal_holds ist historisiert (kein UPDATE der Sperr-Zeile selbst -- ausser released_at/released_by beim Aufheben) — jede Sperre und jede -- Aufhebung bleibt dauerhaft nachvollziehbar (Akzeptanzkriterium 3: -- Aufheben ist selbst protokolliert). Ein Partial-Unique-Index erlaubt -- je Objekt hoechstens EINE AKTIVE Sperre gleichzeitig. CREATE TABLE IF NOT EXISTS legal_holds ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), retention_object_id UUID NOT NULL REFERENCES retention_objects(id) ON DELETE CASCADE, reason TEXT NOT NULL, set_by TEXT NOT NULL, set_at TIMESTAMPTZ NOT NULL DEFAULT now(), released_at TIMESTAMPTZ, released_by TEXT ); CREATE UNIQUE INDEX IF NOT EXISTS idx_legal_holds_active ON legal_holds (retention_object_id) WHERE released_at IS NULL; -- destruction_log ist APPEND-ONLY, real durchgesetzt ueber Trigger (nicht -- nur Anwendungscode) — Akzeptanzkriterium 3 / Pflichtpruefung 2 verlangt -- ausdruecklich "DB-Constraint oder Trigger". CREATE TABLE IF NOT EXISTS destruction_log ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), retention_object_id UUID NOT NULL REFERENCES retention_objects(id) ON DELETE RESTRICT, object_type TEXT NOT NULL, object_reference TEXT NOT NULL, destroyed_at TIMESTAMPTZ NOT NULL DEFAULT now(), destroyed_by TEXT NOT NULL ); CREATE OR REPLACE FUNCTION destruction_log_immutable() RETURNS TRIGGER AS $$ BEGIN RAISE EXCEPTION 'destruction_log ist unveraenderlich (RET-03) - % auf id=% nicht erlaubt', TG_OP, OLD.id; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS trg_destruction_log_no_update ON destruction_log; CREATE TRIGGER trg_destruction_log_no_update BEFORE UPDATE ON destruction_log FOR EACH ROW EXECUTE FUNCTION destruction_log_immutable(); DROP TRIGGER IF EXISTS trg_destruction_log_no_delete ON destruction_log; CREATE TRIGGER trg_destruction_log_no_delete BEFORE DELETE ON destruction_log FOR EACH ROW EXECUTE FUNCTION destruction_log_immutable();