From 990ebdbc843109a6829cca1a728b3e03fe6a00e7 Mon Sep 17 00:00:00 2001 From: Andrei Laas Date: Tue, 29 Sep 2026 18:57:56 +0200 Subject: [PATCH] Versetzung und Befoerderung am Tag, an dem die Besetzung begann Wer heute schon einmal versetzt wurde, liess sich heute nicht noch einmal versetzen oder befoerdern: die laufende Besetzung beginnt dann heute, und der Code schliesst sie auf denselben Tag. chk_assignment_range verlangt aber valid_to > valid_from -- ein Intervall von null Tagen ist verboten, und der Vorgang brach mit der rohen Datenbankmeldung ab. Das Verbot ist richtig, und der Fall ist im Haus schon einmal entschieden worden: terminate_employee loescht die Besetzung beim Grund "No Show", statt sie auf [Eintritt, Eintritt) zu schliessen -- niemand hatte sie je inne. Dieselbe Begruendung gilt hier. Die zweite Versetzung am selben Tag abzuweisen waere genau der haeufigste Fall gewesen: jemand hat die falsche Planstelle erwischt und will es sofort richtigstellen. Beide Ereignisse bleiben in der Historie stehen. Vier Stellen mit demselben Paar aus Schliessen und Einfuegen: transfer_employee, promote_employee und im Nachtlauf die Zweige transfer und promotion. Alle vier aus der laufenden Definition gelesen und an einem Anker ergaenzt; die Selbstpruefung zaehlt die beiden Stellen im Nachtlauf, statt nur ihr Vorhandensein zu pruefen -- der Zweig transfer ist dort schon einmal spurlos verschwunden. --- ...20260929180000_besetzung_am_selben_tag.sql | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 db/migrations/20260929180000_besetzung_am_selben_tag.sql diff --git a/db/migrations/20260929180000_besetzung_am_selben_tag.sql b/db/migrations/20260929180000_besetzung_am_selben_tag.sql new file mode 100644 index 0000000..a3320f0 --- /dev/null +++ b/db/migrations/20260929180000_besetzung_am_selben_tag.sql @@ -0,0 +1,177 @@ +-- Versetzung und Beförderung am selben Tag, an dem die Besetzung begann +-- +-- Gemeldet beim Nachtest am 29.09.: eine Beförderung auf eine andere +-- Planstelle scheiterte mit +-- +-- new row for relation "position_assignments" violates check constraint +-- "chk_assignment_range" +-- +-- Die Person war am selben Tag schon einmal versetzt worden. Ihre laufende +-- Besetzung begann also heute, und der Code schliesst sie auf denselben Tag: +-- +-- update position_assignments set valid_to = where valid_to is null; +-- insert into position_assignments (…) values (…, ); +-- +-- `chk_assignment_range` verlangt `valid_to > valid_from`; ein Intervall von +-- null Tagen ist verboten. Das ist richtig so — und der Fall ist im Haus auch +-- schon einmal gelöst worden: terminate_employee **löscht** die Besetzung beim +-- Grund „No Show", statt sie auf [Eintritt, Eintritt) zu schliessen, mit der +-- Begründung, dass niemand sie je innehatte. Dieselbe Begründung gilt hier: +-- eine Besetzung, die am Tag ihres Beginns wieder endet, hat nicht +-- stattgefunden. +-- +-- Die Alternative — die zweite Versetzung am selben Tag abzuweisen — wäre +-- genau der häufigste Fall: jemand hat die falsche Planstelle erwischt und +-- will es sofort richtigstellen. Die Ereignisse in employee_history bleiben +-- beide stehen; sichtbar ist also weiterhin, dass zweimal verschoben wurde. +-- +-- Betroffen sind vier Stellen mit demselben Paar aus Schliessen und Einfügen: +-- transfer_employee, promote_employee und im Nachtlauf die Zweige `transfer` +-- und `promotion`. Alle vier werden aus der laufenden Definition gelesen und +-- an einem Anker ergänzt. + +-- ═══ 1. transfer_employee ═════════════════════════════════════════ +do $migration$ +declare + v_alt constant text := $anker$update position_assignments set valid_to = v_effective$anker$; + v_neu constant text := $anker$delete from position_assignments + where employee_id = v_employee_id and valid_to is null and valid_from >= v_effective; + update position_assignments set valid_to = v_effective$anker$; + v_def text; + v_anzahl int; +begin + select pg_get_functiondef(p.oid) into v_def + from pg_proc p join pg_namespace n on n.oid = p.pronamespace + where n.nspname = 'public' and p.proname = 'transfer_employee' and p.prokind = 'f'; + + if v_def is null then + raise exception 'transfer_employee ist nicht vorhanden.'; + end if; + + v_anzahl := (length(v_def) - length(replace(v_def, v_alt, ''))) / length(v_alt); + if v_anzahl <> 1 then + raise exception 'Der Anker in transfer_employee kommt % mal vor, erwartet einmal.', v_anzahl; + end if; + + execute replace(v_def, v_alt, v_neu); +end +$migration$; + +-- ═══ 2. promote_employee ══════════════════════════════════════════ +do $migration$ +declare + v_alt constant text := $anker$update position_assignments set valid_to = v_effective_date$anker$; + v_neu constant text := $anker$delete from position_assignments + where employee_id = v_employee_id and valid_to is null and valid_from >= v_effective_date; + update position_assignments set valid_to = v_effective_date$anker$; + v_def text; + v_anzahl int; +begin + select pg_get_functiondef(p.oid) into v_def + from pg_proc p join pg_namespace n on n.oid = p.pronamespace + where n.nspname = 'public' and p.proname = 'promote_employee' and p.prokind = 'f'; + + if v_def is null then + raise exception 'promote_employee ist nicht vorhanden.'; + end if; + + v_anzahl := (length(v_def) - length(replace(v_def, v_alt, ''))) / length(v_alt); + if v_anzahl <> 1 then + raise exception 'Der Anker in promote_employee kommt % mal vor, erwartet einmal.', v_anzahl; + end if; + + execute replace(v_def, v_alt, v_neu); +end +$migration$; + +-- ═══ 3. Der Nachtlauf, beide Zweige ═══════════════════════════════ +-- +-- Hier steht dasselbe zweimal — bei `transfer` und bei `promotion` mit +-- Zielplanstelle. Beide werden ersetzt, und die Zahl wird geprüft: der Zweig +-- `transfer` ist in diesem Projekt schon einmal spurlos verschwunden. +do $migration$ +declare + v_alt constant text := $anker$update position_assignments set valid_to = v_rec.effective_date$anker$; + v_neu constant text := $anker$delete from position_assignments + where employee_id = v_rec.employee_id and valid_to is null and valid_from >= v_rec.effective_date; + update position_assignments set valid_to = v_rec.effective_date$anker$; + v_def text; + v_anzahl int; +begin + select pg_get_functiondef(p.oid) into v_def + from pg_proc p join pg_namespace n on n.oid = p.pronamespace + where n.nspname = 'public' and p.proname = 'apply_due_pending_changes' and p.prokind = 'f'; + + if v_def is null then + raise exception 'apply_due_pending_changes ist nicht vorhanden.'; + end if; + + v_anzahl := (length(v_def) - length(replace(v_def, v_alt, ''))) / length(v_alt); + if v_anzahl <> 2 then + raise exception 'Der Anker im Nachtlauf kommt % mal vor, erwartet zweimal.', v_anzahl; + end if; + + execute replace(v_def, v_alt, v_neu); +end +$migration$; + + +-- ═══ 4. Selbstprüfung ═════════════════════════════════════════════ +do $$ +declare + v_def text; + v_anzahl int; +begin + select pg_get_functiondef(p.oid) into v_def + from pg_proc p join pg_namespace n on n.oid = p.pronamespace + where n.nspname = 'public' and p.proname = 'transfer_employee' and p.prokind = 'f'; + if v_def not like '%valid_from >= v_effective%' then + raise exception 'transfer_employee raeumt die Besetzung vom selben Tag nicht ab.'; + end if; + if v_def not like '%require_hr_admin()%' then + raise exception 'transfer_employee prueft die Rechte nicht.'; + end if; + + select pg_get_functiondef(p.oid) into v_def + from pg_proc p join pg_namespace n on n.oid = p.pronamespace + where n.nspname = 'public' and p.proname = 'promote_employee' and p.prokind = 'f'; + if v_def not like '%valid_from >= v_effective_date%' then + raise exception 'promote_employee raeumt die Besetzung vom selben Tag nicht ab.'; + end if; + if v_def not like '%require_hr_admin()%' then + raise exception 'promote_employee prueft die Rechte nicht.'; + end if; + -- Was an dieser Funktion schon einmal verlorenging. + if v_def not like '%target_position_id%' then + raise exception 'promote_employee hat die Zielplanstelle verloren.'; + end if; + + select pg_get_functiondef(p.oid) into v_def + from pg_proc p join pg_namespace n on n.oid = p.pronamespace + where n.nspname = 'public' and p.proname = 'apply_due_pending_changes' and p.prokind = 'f'; + + v_anzahl := (length(v_def) - length(replace(v_def, 'valid_from >= v_rec.effective_date', ''))) + / length('valid_from >= v_rec.effective_date'); + if v_anzahl <> 2 then + raise exception 'Der Nachtlauf raeumt an % von 2 Stellen ab.', v_anzahl; + end if; + + v_anzahl := (length(v_def) - length(replace(v_def, 'insert into position_assignments', ''))) + / length('insert into position_assignments'); + if v_anzahl <> 2 then + raise exception 'Der Nachtlauf haengt die Besetzung % mal um, erwartet zweimal.', v_anzahl; + end if; + + if v_def not like '%v_rec.payload->''role''%' then + raise exception 'Der Nachtlauf hat die Gruppe role verloren.'; + end if; + if not exists ( + select 1 from pg_proc p join pg_namespace n on n.oid = p.pronamespace + where n.nspname = 'public' and p.proname = 'apply_due_pending_changes' and p.prosecdef + ) then + raise exception 'Der Nachtlauf ist nicht mehr SECURITY DEFINER.'; + end if; + + raise notice 'Besetzung am selben Tag: alle vier Stellen abgeraeumt.'; +end +$$;