-- Zurücksetzen in einem Zug, nicht Feld für Feld. -- -- Die erste Fassung schrieb je Feld ein eigenes UPDATE. Das ist bei -- unabhängigen Feldern harmlos und bei gekoppelten falsch: chk_weekly_hours -- verlangt, dass Beschäftigungsausmaß und Wochenstunden zueinander passen -- (Vollzeit genau 38,5; Teilzeit dazwischen). Wird zuerst das Ausmaß auf -- „Vollzeit" zurückgesetzt, während noch 37 Stunden dastehen, verbietet die -- Bedingung genau diesen Zwischenstand — und das Löschen scheiterte mit -- „new row for relation employees violates check constraint". -- -- Es traf jede Rücknahme einer Vertragsänderung, die beide Felder betraf, -- also praktisch jede: die Oberfläche ändert Ausmaß und Stunden zusammen. -- -- Jetzt werden die Zuweisungen gesammelt und in einer einzigen Anweisung -- geschrieben. Damit entsteht der verbotene Zwischenstand gar nicht — der -- Zustand von davor war ja gültig, sonst stünde er nicht in der Historie. -- -- Bleibt danach doch eine Verletzung, ist sie echt: dann hat eine spätere -- Änderung eines der gekoppelten Felder einzeln angefasst, und der alte Wert -- passt nicht mehr zum heutigen Stand. Dieser Fall wird abgefangen und als -- Satz gemeldet, statt als Datenbankfehler durchzuschlagen. CREATE OR REPLACE FUNCTION public.delete_history_entry(payload jsonb) RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public', 'pg_temp' AS $function$ declare v_id uuid := (payload->>'history_id')::uuid; v_eintrag employee_history%rowtype; v_name text; v_aenderung jsonb; v_feld text; v_wert text; v_spalte text; v_typ text; v_spaeter boolean; v_zurueckgesetzt jsonb := '[]'::jsonb; v_setz text[] := '{}'; -- Feldbeschriftung → Spalte und Typ. Geschlossene Liste: was -- change_employee_data schreiben kann, steht hier, sonst nichts. Der -- Spaltenname geht in dynamisches SQL, deshalb darf er nur von hier kommen. v_karte constant jsonb := jsonb_build_object( 'Vorname', jsonb_build_array('first_name', 'text'), 'Nachname', jsonb_build_array('last_name', 'text'), 'Geschlecht', jsonb_build_array('gender', 'gender_type'), 'Geburtsdatum', jsonb_build_array('birth_date', 'date'), 'SV-Nummer', jsonb_build_array('sv_nummer', 'text'), 'Staatsbürgerschaft', jsonb_build_array('nationality', 'text'), 'Adresse', jsonb_build_array('address', 'text'), 'Postleitzahl', jsonb_build_array('postal_code', 'text'), 'Ort', jsonb_build_array('city', 'text'), 'Land', jsonb_build_array('address_country', 'text'), 'E-Mail', jsonb_build_array('email', 'text'), 'Telefon', jsonb_build_array('phone', 'text'), 'Notfallkontakt', jsonb_build_array('emergency_contact_name', 'text'), 'Notfallkontakt Telefon', jsonb_build_array('emergency_contact_phone', 'text'), 'Notfallkontakt Verhältnis', jsonb_build_array('emergency_contact_relation', 'text'), 'Titel (vorangestellt)', jsonb_build_array('title_prefix', 'liste'), 'Titel (nachgestellt)', jsonb_build_array('title_suffix', 'liste'), 'Beschäftigungsausmaß', jsonb_build_array('employment_type', 'employment_type'), 'Wochenstunden', jsonb_build_array('weekly_hours', 'numeric'), 'Vertragsart', jsonb_build_array('contract_type', 'contract_type'), 'Befristet bis', jsonb_build_array('contract_end_date', 'date'), 'Angestellte:r/Arbeiter:in', jsonb_build_array('worker_type', 'worker_type'), 'Kollektivvertrag', jsonb_build_array('collective_agreement', 'collective_agreement'), 'Arbeitstage', jsonb_build_array('work_days', 'liste'), 'Betriebsrat', jsonb_build_array('is_betriebsrat', 'boolean'), 'Dienstwagen', jsonb_build_array('has_dienstwagen', 'boolean'), 'Laterale Führung', jsonb_build_array('is_laterale_fuehrung', 'boolean'), 'C-Level', jsonb_build_array('is_c_level', 'boolean'), 'Dienstwagen Antrieb', jsonb_build_array('dienstwagen_art', 'text') ); begin perform require_hr_admin(); select * into v_eintrag from employee_history where id = v_id; if not found then raise exception 'Historieneintrag nicht gefunden.'; end if; if v_eintrag.event_type = 'Eintritt' then raise exception 'Der Eintritt lässt sich nicht löschen — er ist der Anfang der Zeitleiste.'; end if; if v_eintrag.event_type not in ('Stammdatenänderung', 'Vertragsänderung') then raise exception 'Nur Stammdaten- und Vertragsänderungen lassen sich hier zurücknehmen. Für % gibt es den passenden Vorgang.', v_eintrag.event_type; end if; if v_eintrag.event_date > current_date then raise exception 'Diese Änderung ist noch nicht wirksam und hängt an einem geplanten Vorgang. Sie muss dort abgebrochen werden.'; end if; if v_eintrag.changes is null or jsonb_array_length(v_eintrag.changes) = 0 then raise exception 'Zu diesem Eintrag sind keine Feldwerte erfasst — es gibt nichts, worauf zurückgesetzt werden könnte.'; end if; select first_name || ' ' || last_name into v_name from employees where id = v_eintrag.employee_id; -- Je Feld: nur zurücksetzen, wenn kein späterer Eintrag dasselbe Feld -- angefasst hat. Sonst gilt der spätere Wert weiter. for v_aenderung in select * from jsonb_array_elements(v_eintrag.changes) loop v_feld := v_aenderung->>'feld'; if not v_karte ? v_feld then continue; -- unbekannte Beschriftung: nichts anfassen end if; select exists ( select 1 from employee_history h, lateral jsonb_array_elements(coalesce(h.changes, '[]'::jsonb)) a where h.employee_id = v_eintrag.employee_id and h.id <> v_eintrag.id and a->>'feld' = v_feld and (h.event_date, h.created_at) > (v_eintrag.event_date, v_eintrag.created_at) ) into v_spaeter; if v_spaeter then continue; end if; v_spalte := v_karte->v_feld->>0; v_typ := v_karte->v_feld->>1; v_wert := v_aenderung->>'vorher'; if v_typ = 'liste' then v_setz := v_setz || format('%I = coalesce(string_to_array(%L, '', ''), ''{}'')', v_spalte, nullif(v_wert, '')); else v_setz := v_setz || format('%I = %L::%s', v_spalte, nullif(v_wert, ''), v_typ); end if; v_zurueckgesetzt := v_zurueckgesetzt || jsonb_build_object( 'feld', v_feld, 'vorher', v_aenderung->>'nachher', 'nachher', v_wert ); end loop; -- Alle Felder in *einem* UPDATE. Einzeln nacheinander zu schreiben war der -- Fehler der ersten Fassung: chk_weekly_hours verknüpft Beschäftigungsausmaß -- und Wochenstunden, und zwischen zwei getrennten Anweisungen steht -- zwangsläufig ein Zwischenstand, den die Bedingung verbietet — „Vollzeit -- mit 37 Stunden". Gemeinsam gesetzt gibt es diesen Zwischenstand nicht. if array_length(v_setz, 1) > 0 then begin execute format('update employees set %s where id = %L', array_to_string(v_setz, ', '), v_eintrag.employee_id); exception when check_violation then -- Bleibt trotzdem etwas übrig: dann wurde eines von zwei zusammen- -- gehörenden Feldern später einzeln geändert, und der alte Wert passt -- nicht mehr zum heutigen Stand. Lieber verständlich abweisen. raise exception 'Zurücksetzen nicht möglich: die Werte von damals passen nicht mehr zum heutigen Stand (%). Vermutlich wurde ein zusammengehörendes Feld später einzeln geändert.', sqlerrm; end; end if; delete from employee_history where id = v_id; insert into audit_log (actor_user_id, actor_name, action, target_label, target_employee_id, details, changes) values (app_current_user_id(), current_actor_name(), 'Historieneintrag gelöscht', v_name, v_eintrag.employee_id, v_eintrag.event_type || ' vom ' || v_eintrag.event_date || case when jsonb_array_length(v_zurueckgesetzt) = 0 then ' gelöscht; keine Werte zurückgesetzt (spätere Änderungen gelten)' else ' gelöscht und zurückgesetzt: ' || app_aenderungsfelder(v_zurueckgesetzt) end, v_zurueckgesetzt); end; $function$; -- Selbstprüfung. do $$ declare v_def text := pg_get_functiondef('public.delete_history_entry(jsonb)'::regprocedure); begin if v_def like '%update employees set %I%' then raise exception 'Es wird noch je Feld einzeln geschrieben'; end if; if v_def not like '%array_to_string(v_setz%' then raise exception 'Das gesammelte UPDATE fehlt'; end if; if v_def not like '%check_violation%' then raise exception 'Die Verletzung wird nicht abgefangen'; end if; end $$;